├── .github └── workflows │ ├── codeql.yaml │ ├── docs.yml │ └── test.yaml ├── .gitignore ├── .goreleaser.yaml ├── ABTaskFile ├── Dockerfile.goreleaser ├── LICENSE ├── README.md ├── ajc ├── info_command.go ├── main.go ├── package_command.go ├── queue_command.go ├── task_command.go ├── task_cron_command.go └── util.go ├── asyncjobs.go ├── client.go ├── client_examples_test.go ├── client_options.go ├── client_test.go ├── docs ├── .hugo_build.lock ├── archetypes │ └── default.md ├── config.toml ├── content │ ├── _index.md │ ├── overview │ │ ├── cli-overview.md │ │ ├── features.md │ │ ├── golang-overview.md │ │ ├── handlers-docker.md │ │ ├── handlers-k8s.md │ │ └── scheduled-tasks.md │ └── reference │ │ ├── lifecycle-events.md │ │ ├── request-reply.md │ │ ├── routing-concurrency-retry.md │ │ ├── security.md │ │ ├── task-lifecycle.md │ │ └── terminology.md ├── layouts │ └── partials │ │ └── logo.html ├── static │ ├── css │ │ ├── theme-asyncjobs.css │ │ └── theme-my-custom-variant.css │ └── logo.png └── themes │ └── hugo-theme-relearn │ ├── .editorconfig │ ├── .gitignore │ ├── .grenrc.js │ ├── .issuetracker │ ├── LICENSE │ ├── README.md │ ├── archetypes │ ├── chapter.md │ └── default.md │ ├── config.toml │ ├── i18n │ ├── ar.toml │ ├── de.toml │ ├── en.toml │ ├── es.toml │ ├── fr.toml │ ├── hi.toml │ ├── id.toml │ ├── ja.toml │ ├── kr.toml │ ├── nl.toml │ ├── pir.toml │ ├── pt.toml │ ├── ru.toml │ ├── tr.toml │ ├── vn.toml │ ├── zh-cn.toml │ └── zh-tw.toml │ ├── images │ ├── screenshot.png │ └── tn.png │ ├── layouts │ ├── 404.html │ ├── _default │ │ ├── _markup │ │ │ └── render-codeblock-mermaid.html │ │ ├── list.html │ │ ├── list.print.html │ │ ├── rss.xml │ │ ├── single.html │ │ ├── single.print.html │ │ ├── sitemap.xml │ │ ├── taxonomy.html │ │ └── taxonomy.print.html │ ├── index.html │ ├── index.json │ ├── index.print.html │ ├── partials │ │ ├── article.html │ │ ├── content-footer.html │ │ ├── content-print.html │ │ ├── content-screen.html │ │ ├── content.html │ │ ├── custom-comments.html │ │ ├── custom-footer.html │ │ ├── custom-header.html │ │ ├── favicon.html │ │ ├── footer.html │ │ ├── header.html │ │ ├── logo.html │ │ ├── menu-footer.html │ │ ├── menu-post.html │ │ ├── menu-pre.html │ │ ├── menu.html │ │ ├── meta.html │ │ ├── page-meta.got │ │ ├── search.html │ │ ├── stylesheet.html │ │ ├── tags.html │ │ ├── toc.html │ │ └── version.html │ └── shortcodes │ │ ├── attachments.html │ │ ├── button.html │ │ ├── children.html │ │ ├── expand.html │ │ ├── include.html │ │ ├── mermaid.html │ │ ├── notice.html │ │ ├── siteparam.html │ │ ├── swagger.html │ │ ├── tab.html │ │ └── tabs.html │ ├── static │ ├── css │ │ ├── auto-complete.css │ │ ├── chroma-learn.css │ │ ├── chroma-neon.css │ │ ├── chroma-relearn-dark.css │ │ ├── chroma-relearn-light.css │ │ ├── featherlight.min.css │ │ ├── fontawesome-all.min.css │ │ ├── nucleus.css │ │ ├── perfect-scrollbar.min.css │ │ ├── print.css │ │ ├── tabs.css │ │ ├── tags.css │ │ ├── theme-blue.css │ │ ├── theme-green.css │ │ ├── theme-learn.css │ │ ├── theme-neon.css │ │ ├── theme-red.css │ │ ├── theme-relearn-dark.css │ │ ├── theme-relearn-light.css │ │ ├── theme-relearn.css │ │ ├── theme.css │ │ └── variant.css │ ├── fonts │ │ ├── WorkSans-Bold.woff │ │ ├── WorkSans-Bold.woff2 │ │ ├── WorkSans-ExtraLight.woff │ │ ├── WorkSans-ExtraLight.woff2 │ │ ├── WorkSans-Light.woff │ │ ├── WorkSans-Light.woff2 │ │ ├── WorkSans-Medium.woff │ │ ├── WorkSans-Medium.woff2 │ │ ├── WorkSans-Regular.woff │ │ └── WorkSans-Regular.woff2 │ ├── images │ │ └── gopher-404.jpg │ ├── js │ │ ├── auto-complete.js │ │ ├── clipboard.min.js │ │ ├── featherlight.min.js │ │ ├── jquery.min.js │ │ ├── jquery.svg.pan.zoom.js │ │ ├── lunr.min.js │ │ ├── mermaid.min.js │ │ ├── perfect-scrollbar.min.js │ │ ├── rapidoc-min.js │ │ ├── search.js │ │ ├── theme.js │ │ └── variant.js │ └── webfonts │ │ ├── fa-brands-400.eot │ │ ├── fa-brands-400.svg │ │ ├── fa-brands-400.ttf │ │ ├── fa-brands-400.woff │ │ ├── fa-brands-400.woff2 │ │ ├── fa-regular-400.eot │ │ ├── fa-regular-400.svg │ │ ├── fa-regular-400.ttf │ │ ├── fa-regular-400.woff │ │ ├── fa-regular-400.woff2 │ │ ├── fa-solid-900.eot │ │ ├── fa-solid-900.svg │ │ ├── fa-solid-900.ttf │ │ ├── fa-solid-900.woff │ │ └── fa-solid-900.woff2 │ └── theme.toml ├── election ├── election.go ├── election_test.go ├── options.go └── stats.go ├── errors.go ├── event_test.go ├── generators ├── fs │ └── godocker │ │ ├── Dockerfile.templ │ │ └── main.go.templ ├── godocker.go └── package.go ├── go.mod ├── go.sum ├── lifecycle.go ├── logger.go ├── mux.go ├── mux_test.go ├── processor.go ├── processor_test.go ├── queue.go ├── request_reply_handler.go ├── request_reply_handler_test.go ├── retrypolicy.go ├── retrypolicy_test.go ├── scheduled_task.go ├── stats.go ├── storage.go ├── storage_test.go ├── task.go ├── task_scheduler.go ├── task_scheduler_test.go ├── task_test.go ├── testdata ├── failing-handler.sh └── passing-handler.sh └── tools.go /.github/workflows/codeql.yaml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ master, main ] 6 | 7 | schedule: 8 | - cron: '40 12 * * 4' 9 | 10 | jobs: 11 | analyze: 12 | name: Analyze 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | language: [ 'go' ] 19 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 20 | # Learn more: 21 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 22 | 23 | steps: 24 | - name: Checkout repository 25 | uses: actions/checkout@v2 26 | 27 | # Initializes the CodeQL tools for scanning. 28 | - name: Initialize CodeQL 29 | uses: github/codeql-action/init@v1 30 | with: 31 | languages: ${{ matrix.language }} 32 | # If you wish to specify custom queries, you can do so here or in a config file. 33 | # By default, queries listed here will override any specified in a config file. 34 | # Prefix the list here with "+" to use these queries and those in the config file. 35 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 36 | 37 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 38 | # If this step fails, then you should remove it and run the build manually (see below) 39 | - name: Autobuild 40 | uses: github/codeql-action/autobuild@v1 41 | 42 | # ℹ️ Command-line programs to run using the OS shell. 43 | # 📚 https://git.io/JvXDl 44 | 45 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 46 | # and modify them (or add more) to build your code if your project 47 | # uses a compiled language 48 | 49 | #- run: | 50 | # make bootstrap 51 | # make release 52 | 53 | - name: Perform CodeQL Analysis 54 | uses: github/codeql-action/analyze@v1 55 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: github pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main # Set a branch to deploy 7 | pull_request: 8 | 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-20.04 12 | steps: 13 | - uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod 16 | 17 | - name: Setup Hugo 18 | uses: peaceiris/actions-hugo@v2 19 | with: 20 | hugo-version: '0.98.0' 21 | 22 | - name: Build 23 | run: hugo --minify --source docs 24 | 25 | - name: Deploy 26 | uses: peaceiris/actions-gh-pages@v3 27 | if: github.ref == 'refs/heads/main' 28 | with: 29 | github_token: ${{ secrets.GITHUB_TOKEN }} 30 | publish_dir: ./docs/public 31 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | on: [ push, pull_request ] 3 | 4 | jobs: 5 | test: 6 | strategy: 7 | matrix: 8 | go: ["1.23", "1.24"] 9 | 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v2 14 | 15 | - name: Setup Go 16 | uses: actions/setup-go@v2 17 | with: 18 | go-version: ${{matrix.go}} 19 | 20 | - name: Lint and Test 21 | uses: choria-io/actions/lint_and_test/go@main 22 | with: 23 | ginkgo: "v2" 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | ajc/ajc 3 | dist 4 | .hugo_build.lock 5 | docs/public 6 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | project_name: asyncjobs 2 | 3 | release: 4 | github: 5 | owner: choria-io 6 | name: asyncjobs 7 | name_template: "Release {{.Version}}" 8 | draft: true 9 | 10 | changelog: 11 | skip: true 12 | 13 | builds: 14 | - main: ./ajc 15 | id: ajc 16 | binary: ajc 17 | goos: 18 | - darwin 19 | - linux 20 | - windows 21 | goarch: 22 | - amd64 23 | - arm 24 | - arm64 25 | goarm: 26 | - "6" 27 | - "7" 28 | 29 | archives: 30 | - name_template: "asyncjobs-{{.Version}}-{{.Os}}-{{.Arch}}{{if .Arm}}{{.Arm}}{{end}}" 31 | wrap_in_directory: true 32 | format: tar.gz 33 | format_overrides: 34 | - goos: windows 35 | format: zip 36 | files: 37 | - README.md 38 | - LICENSE 39 | 40 | dockers: 41 | - goos: linux 42 | goarch: amd64 43 | skip_push: true 44 | dockerfile: Dockerfile.goreleaser 45 | image_templates: 46 | - "choria/asyncjobs:latest" 47 | - "choria/asyncjobs:{{ .Version }}" 48 | build_flag_templates: 49 | - "--pull" 50 | - "--label=org.opencontainers.image.created={{.Date}}" 51 | - "--label=org.opencontainers.image.title={{.ProjectName}}" 52 | - "--label=org.opencontainers.image.revision={{.FullCommit}}" 53 | - "--label=org.opencontainers.image.version={{.Version}}" 54 | 55 | checksum: 56 | name_template: "SHA256SUMS" 57 | algorithm: sha256 58 | 59 | nfpms: 60 | - file_name_template: 'asyncjobs-{{.Version}}-{{.Arch}}{{if .Arm}}{{.Arm}}{{end}}' 61 | homepage: https://github.com/choria-io/asyncjobs 62 | description: Choria Asynchronous Jobs CLI 63 | maintainer: R.I. Pienaar 64 | license: Apache 2.0 65 | vendor: Choria 66 | formats: 67 | - deb 68 | - rpm 69 | -------------------------------------------------------------------------------- /Dockerfile.goreleaser: -------------------------------------------------------------------------------- 1 | # use goreleaser to build 2 | 3 | FROM alpine:latest 4 | 5 | RUN apk --no-cache add ca-certificates && \ 6 | addgroup -g 2048 asyncjobs && \ 7 | adduser -u 2048 -h /home/asyncjobs -g "Choria Asynchronous Jobs" -S -D -H -G asyncjobs asyncjobs 8 | 9 | USER asyncjobs 10 | ENTRYPOINT ["/usr/bin/ajc"] 11 | COPY ajc /usr/bin/ajc 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Choria Asynchronous Jos](https://choria.io/async-logo-horizontal.png) 2 | 3 | ## Overview 4 | 5 | This is an Asynchronous Job Queue system that relies on NATS JetStream for storage and general job life cycle management. 6 | It is compatible with any NATS JetStream based system like a private hosted JetStream, Choria Streams or a commercial SaaS. 7 | 8 | Each Task is stored in JetStream by a unique ID and Work Queue item is made referencing that Task. JetStream will handle 9 | dealing with scheduling, retries, acknowledgements and more of the Work Queue item. The stored Task will be updated 10 | during the lifecycle. 11 | 12 | Multiple processes can process jobs concurrently, thus job processing is both horizontally and vertically scalable. Job 13 | handlers are implemented in Go with one process hosting one or many handlers. Other languages can implement Job Handlers using 14 | NATS Request-Reply services. Per process concurrency and overall per-queue concurrency controls exist. 15 | 16 | This package heavily inspired by [hibiken/asynq](https://github.com/hibiken/asynq/). 17 | 18 | * [Documentation](https://choria-io.github.io/asyncjobs/) 19 | * [Community](https://github.com/choria-io/asyncjobs/discussions) 20 | * Examples 21 | * [Golang](https://choria-io.github.io/asyncjobs/overview/golang-overview/) 22 | * [CLI](https://choria-io.github.io/asyncjobs/overview/cli-overview/) 23 | 24 | [![Go Reference](https://pkg.go.dev/badge/github.com/choria-io/asyncjobs.svg)](https://pkg.go.dev/github.com/choria-io/asyncjobs) 25 | [![Go Report Card](https://goreportcard.com/badge/github.com/choria-io/asyncjobs)](https://goreportcard.com/report/github.com/choria-io/asyncjobs) 26 | [![CodeQL](https://github.com/choria-io/asyncjobs/workflows/CodeQL/badge.svg)](https://github.com/choria-io/asyncjobs/actions/workflows/codeql.yaml) 27 | [![Unit Tests](https://github.com/choria-io/asyncjobs/actions/workflows/test.yaml/badge.svg)](https://github.com/choria-io/asyncjobs/actions/workflows/test.yaml) 28 | 29 | ## Status 30 | 31 | This is a brand-new project, under heavy development. The core Task handling is in good shape and reasonably stable. Task Scheduler is still subject to some change. 32 | 33 | ## Synopsis 34 | 35 | Tasks are published to Work Queues: 36 | 37 | ```go 38 | // establish a connection to the EMAIL work queue using a NATS context 39 | client, _ := asyncjobs.NewClient(asyncjobs.NatsConn(nc), asyncjobs.BindWorkQueue("EMAIL")) 40 | 41 | // create a task with the type 'email:new' and body from newEmail() 42 | task, _ := asyncjobs.NewTask("email:new", newEmail()) 43 | 44 | // store it in the Work Queue 45 | client.EnqueueTask(ctx, task) 46 | ``` 47 | 48 | Tasks are processes by horizontally and vertically scalable processes. Typically, a Handler handles one type of Task. We have Prometheus 49 | integration, concurrency and backoffs configured. 50 | 51 | ```go 52 | // establish a connection to the EMAIL work queue using a 53 | // NATS context, with concurrency, prometheus stats and backoff 54 | client, _ := asyncjobs.NewClient( 55 | asyncjobs.NatsContext("EMAIL"), 56 | asyncjobs.BindWorkQueue("EMAIL"), 57 | asyncjobs.ClientConcurrency(10), 58 | asyncjobs.PrometheusListenPort(8080), 59 | asyncjobs.RetryBackoffPolicy(asyncjobs.RetryLinearTenMinutes)) 60 | 61 | router := asyncjobs.NewTaskRouter() 62 | router.Handler("email:new", func(ctx context.Context, log asyncjobs.Logger, task *asyncjobs.Task) (any, error) { 63 | log.Printf("Processing task %s", task.ID) 64 | 65 | // do work here using task.Payload 66 | 67 | return "sent", nil 68 | }) 69 | 70 | client.Run(ctx, router) 71 | ``` 72 | 73 | See our [documentation](https://choria-io.github.io/asyncjobs/) for a deep dive into the use cases, architecture, abilities and more. 74 | 75 | ## Requirements 76 | 77 | NATS 2.8.0 or newer with JetStream enabled. 78 | 79 | ## Features 80 | 81 | See the [Feature List](https://choria-io.github.io/asyncjobs/overview/features/) page for a full feature break down. 82 | -------------------------------------------------------------------------------- /ajc/info_command.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, R.I. Pienaar and the Project contributors 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package main 6 | 7 | import ( 8 | "fmt" 9 | "time" 10 | 11 | "github.com/choria-io/asyncjobs" 12 | "github.com/choria-io/fisk" 13 | ) 14 | 15 | type infoCommand struct { 16 | replicas uint 17 | memory bool 18 | retention time.Duration 19 | } 20 | 21 | func configureInfoCommand(app *fisk.Application) { 22 | c := &infoCommand{} 23 | 24 | info := app.Command("info", "Shows general Task and Queue information").Action(c.infoAction) 25 | info.Flag("replica", "When initializing, do so with this many replicas").Short('R').Default("1").UintVar(&c.replicas) 26 | info.Flag("memory", "When initializing, do so with memory storage").UnNegatableBoolVar(&c.memory) 27 | info.Flag("retention", "When initializing, sets how long Tasks are kept").DurationVar(&c.retention) 28 | } 29 | 30 | func (c *infoCommand) infoAction(_ *fisk.ParseContext) error { 31 | opts := []asyncjobs.ClientOpt{asyncjobs.StoreReplicas(c.replicas), asyncjobs.TaskRetention(c.retention)} 32 | if c.memory { 33 | opts = append(opts, asyncjobs.MemoryStorage()) 34 | } 35 | 36 | err := prepare(opts...) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | tasks, err := admin.TasksInfo() 42 | if err != nil { 43 | return err 44 | } 45 | 46 | showTasks(tasks) 47 | fmt.Println() 48 | 49 | cfg, err := admin.ConfigurationInfo() 50 | if err != nil { 51 | return err 52 | } 53 | showConfig(cfg) 54 | fmt.Println() 55 | 56 | es, err := admin.ElectionStorage() 57 | if err == nil { 58 | showElectionStatus(es) 59 | fmt.Println() 60 | } 61 | 62 | queues, err := admin.Queues() 63 | if err != nil { 64 | return err 65 | } 66 | 67 | for _, q := range queues { 68 | showQueue(q) 69 | 70 | fmt.Println() 71 | } 72 | return nil 73 | } 74 | -------------------------------------------------------------------------------- /ajc/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, R.I. Pienaar and the Project contributors 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package main 6 | 7 | import ( 8 | "fmt" 9 | "os" 10 | "strings" 11 | 12 | "github.com/choria-io/asyncjobs" 13 | "github.com/choria-io/fisk" 14 | "github.com/nats-io/jsm.go/natscontext" 15 | "github.com/sirupsen/logrus" 16 | ) 17 | 18 | var ( 19 | version = "development" 20 | timeFormat = "02 Jan 06 15:04:05 MST" 21 | 22 | nctx string 23 | debug bool 24 | log *logrus.Entry 25 | client *asyncjobs.Client 26 | admin asyncjobs.StorageAdmin 27 | ajc *fisk.Application 28 | ) 29 | 30 | func main() { 31 | ajc = fisk.New("ajc", "Choria Asynchronous Jobs") 32 | ajc.Version(version) 33 | ajc.Author("R.I.Pienaar ") 34 | ajc.UsageWriter(os.Stdout) 35 | ajc.HelpFlag.Short('h') 36 | 37 | ajc.Flag("context", "NATS Context to use for connecting to JetStream").PlaceHolder("NAME").Envar("CONTEXT").Default("AJC").StringVar(&nctx) 38 | ajc.Flag("debug", "Enable debug level logging").Envar("AJC_DEBUG").BoolVar(&debug) 39 | 40 | configureInfoCommand(ajc) 41 | configureTaskCommand(ajc) 42 | configureQueueCommand(ajc) 43 | configurePackagesCommand(ajc) 44 | 45 | _, err := ajc.Parse(os.Args[1:]) 46 | if err != nil { 47 | switch { 48 | case strings.Contains(err.Error(), "unknown context"): 49 | fmt.Fprintf(os.Stderr, "ajc: no NATS context %q found, create one using 'nats context'\n", nctx) 50 | 51 | known := natscontext.KnownContexts() 52 | if len(known) > 0 { 53 | fmt.Fprintln(os.Stderr) 54 | fmt.Fprintf(os.Stderr, "Known contexts: %s\n", strings.Join(known, "\n ")) 55 | } 56 | 57 | default: 58 | fmt.Fprintf(os.Stderr, "ajc runtime error: %v\n", err) 59 | if err != asyncjobs.ErrNoTasks { 60 | fmt.Fprintln(os.Stderr) 61 | ajc.Usage(os.Args[1:]) 62 | } 63 | } 64 | 65 | os.Exit(1) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /ajc/package_command.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, R.I. Pienaar and the Project contributors 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package main 6 | 7 | import ( 8 | "fmt" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/choria-io/asyncjobs" 14 | "github.com/choria-io/asyncjobs/generators" 15 | "github.com/choria-io/fisk" 16 | "gopkg.in/yaml.v3" 17 | ) 18 | 19 | type packageCommand struct { 20 | file string 21 | } 22 | 23 | func configurePackagesCommand(app *fisk.Application) { 24 | c := &packageCommand{} 25 | 26 | pkg := app.Command("package", "Creates packages hosting handlers").Alias("pkg") 27 | 28 | pkg.Command("docker", "Creates a Docker Container hosting handlers based on handlers.yaml").Action(c.dockerAction) 29 | pkg.Flag("file", "Use a specific configuration file rather than asyncjobs.yaml").Default("asyncjobs.yaml").ExistingFileVar(&c.file) 30 | } 31 | 32 | func (c *packageCommand) dockerAction(_ *fisk.ParseContext) error { 33 | createLogger() 34 | 35 | _, err := os.Stat(c.file) 36 | if os.IsNotExist(err) { 37 | return fmt.Errorf("handlers.yaml does not exist") 38 | } 39 | 40 | hb, err := os.ReadFile(c.file) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | h := &generators.Package{} 46 | err = yaml.Unmarshal(hb, h) 47 | if err != nil { 48 | return fmt.Errorf("invalid handlers file: %v", err) 49 | } 50 | 51 | if h.ContextName == "" { 52 | h.ContextName = "AJ" 53 | } 54 | if h.WorkQueue == "" { 55 | h.WorkQueue = "DEFAULT" 56 | } 57 | if h.AJVersion == "" { 58 | h.AJVersion = fmt.Sprintf("v%s", version) 59 | } 60 | if h.Name == "" { 61 | h.Name = "choria.io/asyncjobs/handlers" 62 | } 63 | if h.RetryPolicy == "" { 64 | h.RetryPolicy = "default" 65 | } 66 | 67 | if len(h.TaskHandlers) == 0 { 68 | return fmt.Errorf("no task handlers specified in %s", c.file) 69 | } 70 | 71 | table := newTableWriter("Handler Microservice Settings") 72 | table.AddRow("Package Name", h.Name) 73 | table.AddRow("NATS Context Name", h.ContextName) 74 | table.AddRow("Work Queue", h.WorkQueue) 75 | table.AddRow("Task Handlers", len(h.TaskHandlers)) 76 | table.AddRow("Retry Backoff Policy", h.RetryPolicy) 77 | if len(h.DiscardStates) > 0 { 78 | table.AddRow("End State Discard", strings.Join(h.DiscardStates, ", ")) 79 | } else { 80 | table.AddRow("End State Discard", "none") 81 | } 82 | table.AddRow("github.com/choria-io/asyncjobs", h.AJVersion) 83 | fmt.Println(table.Render()) 84 | 85 | table = newTableWriter("Handler Configuration and Packages") 86 | table.AddHeaders("Task Type", "Handler Kind", "Detail") 87 | for _, h := range h.TaskHandlers { 88 | switch { 89 | case h.Command != "": 90 | table.AddRow(h.TaskType, "External Command", h.Command) 91 | case h.Package != "": 92 | table.AddRow(h.TaskType, "Go Package", fmt.Sprintf("%s@%s", h.Package, h.Version)) 93 | default: 94 | table.AddRow(h.TaskType, "Request-Reply Service", asyncjobs.RequestReplySubjectForTaskType(h.TaskType)) 95 | } 96 | } 97 | fmt.Println(table.Render()) 98 | fmt.Println() 99 | 100 | generator, err := generators.NewGoContainer(h) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | path, err := filepath.Abs(".") 106 | if err != nil { 107 | return err 108 | } 109 | 110 | err = generator.RenderToDirectory(path) 111 | if err != nil { 112 | return err 113 | } 114 | 115 | fmt.Println("Build your container using 'docker build'") 116 | 117 | return nil 118 | } 119 | -------------------------------------------------------------------------------- /asyncjobs.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, R.I. Pienaar and the Project contributors 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package asyncjobs 6 | 7 | import ( 8 | "context" 9 | "regexp" 10 | "time" 11 | 12 | "github.com/nats-io/jsm.go" 13 | "github.com/nats-io/nats.go" 14 | ) 15 | 16 | const ( 17 | // ShortedScheduledDeadline is the shortest deadline a scheduled task may have 18 | ShortedScheduledDeadline = 30 * time.Second 19 | // DefaultJobRunTime when not configured for a queue this is the default run-time handlers will get 20 | DefaultJobRunTime = time.Hour 21 | // DefaultMaxTries when not configured for a task this is the default tries it will get 22 | DefaultMaxTries = 10 23 | // DefaultQueueMaxConcurrent when not configured for a queue this is the default concurrency setting 24 | DefaultQueueMaxConcurrent = 100 25 | ) 26 | 27 | // StorageAdmin is helpers to support the CLI mainly, this leaks a bunch of details about JetStream 28 | // but that's ok, we're not really intending to change the storage or support more 29 | type StorageAdmin interface { 30 | Queues() ([]*QueueInfo, error) 31 | QueueNames() ([]string, error) 32 | QueueInfo(name string) (*QueueInfo, error) 33 | PurgeQueue(name string) error 34 | DeleteQueue(name string) error 35 | PrepareQueue(q *Queue, replicas int, memory bool) error 36 | ConfigurationInfo() (*nats.KeyValueBucketStatus, error) 37 | PrepareConfigurationStore(memory bool, replicas int) error 38 | PrepareTasks(memory bool, replicas int, retention time.Duration) error 39 | DeleteTaskByID(id string) error 40 | TasksInfo() (*TasksInfo, error) 41 | Tasks(ctx context.Context, limit int32) (chan *Task, error) 42 | TasksStore() (*jsm.Manager, *jsm.Stream, error) 43 | ElectionStorage() (nats.KeyValue, error) 44 | } 45 | 46 | type ScheduledTaskStorage interface { 47 | SaveScheduledTask(st *ScheduledTask, update bool) error 48 | LoadScheduledTaskByName(name string) (*ScheduledTask, error) 49 | DeleteScheduledTaskByName(name string) error 50 | ScheduledTasks(ctx context.Context) ([]*ScheduledTask, error) 51 | ScheduledTasksWatch(ctx context.Context) (chan *ScheduleWatchEntry, error) 52 | EnqueueTask(ctx context.Context, queue *Queue, task *Task) error 53 | ElectionStorage() (nats.KeyValue, error) 54 | PublishLeaderElectedEvent(ctx context.Context, name string, component string) error 55 | } 56 | 57 | // Storage implements the backend access 58 | type Storage interface { 59 | SaveTaskState(ctx context.Context, task *Task, notify bool) error 60 | EnqueueTask(ctx context.Context, queue *Queue, task *Task) error 61 | RetryTaskByID(ctx context.Context, queue *Queue, id string) error 62 | LoadTaskByID(id string) (*Task, error) 63 | DeleteTaskByID(id string) error 64 | PublishTaskStateChangeEvent(ctx context.Context, task *Task) error 65 | AckItem(ctx context.Context, item *ProcessItem) error 66 | NakBlockedItem(ctx context.Context, item *ProcessItem) error 67 | NakItem(ctx context.Context, item *ProcessItem) error 68 | TerminateItem(ctx context.Context, item *ProcessItem) error 69 | PollQueue(ctx context.Context, q *Queue) (*ProcessItem, error) 70 | PrepareQueue(q *Queue, replicas int, memory bool) error 71 | PrepareTasks(memory bool, replicas int, retention time.Duration) error 72 | PrepareConfigurationStore(memory bool, replicas int) error 73 | SaveScheduledTask(st *ScheduledTask, update bool) error 74 | LoadScheduledTaskByName(name string) (*ScheduledTask, error) 75 | DeleteScheduledTaskByName(name string) error 76 | ScheduledTasks(ctx context.Context) ([]*ScheduledTask, error) 77 | ScheduledTasksWatch(ctx context.Context) (chan *ScheduleWatchEntry, error) 78 | } 79 | 80 | var ( 81 | validNameMatcher = regexp.MustCompile(`^[a-zA-Z0-9_:-]+$`) 82 | ) 83 | 84 | // IsValidName is a generic strict name validator for what we want people to put in name - task names etc, things that turn into subjects 85 | func IsValidName(name string) bool { 86 | return validNameMatcher.MatchString(name) 87 | } 88 | -------------------------------------------------------------------------------- /client_examples_test.go: -------------------------------------------------------------------------------- 1 | package asyncjobs 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "time" 8 | ) 9 | 10 | func newEmail(to, subject, body string) map[string]any { 11 | return map[string]any{ 12 | "to": to, 13 | "subject": subject, 14 | "body": body, 15 | } 16 | } 17 | 18 | func panicIfErr(err error) { 19 | if err != nil { 20 | panic(err) 21 | } 22 | } 23 | 24 | func ExampleClient_producer() { 25 | queue := &Queue{ 26 | Name: "P100", 27 | MaxRunTime: 60 * time.Minute, 28 | MaxConcurrent: 20, 29 | MaxTries: 100, 30 | } 31 | 32 | email := newEmail("user@example.net", "Test Subject", "Test Body") 33 | 34 | // Creates a new task that has a deadline for processing 1 hour from now 35 | task, err := NewTask("email:send", email, TaskDeadline(time.Now().Add(time.Hour))) 36 | panicIfErr(err) 37 | 38 | // Uses the NATS CLI context WQ for connection details, will create the queue if 39 | // it does not already exist 40 | client, err := NewClient(NatsContext("WQ"), WorkQueue(queue)) 41 | panicIfErr(err) 42 | 43 | // Adds the task to the queue called P100 44 | err = client.EnqueueTask(context.Background(), task) 45 | panicIfErr(err) 46 | } 47 | 48 | func ExampleNewTask_with_deadline() { 49 | email := newEmail("user@example.net", "Test Subject", "Test Body") 50 | 51 | // Creates a new task that has a deadline for processing 1 hour from now 52 | task, err := NewTask("email:send", email, TaskDeadline(time.Now().Add(time.Hour))) 53 | if err != nil { 54 | panic(fmt.Sprintf("Could not create task: %v", err)) 55 | } 56 | 57 | fmt.Printf("Task ID: %s\n", task.ID) 58 | } 59 | 60 | func ExampleClient_consumer() { 61 | queue := &Queue{ 62 | Name: "P100", 63 | MaxRunTime: 60 * time.Minute, 64 | MaxConcurrent: 20, 65 | MaxTries: 100, 66 | } 67 | 68 | // Uses the NATS CLI context WQ for connection details, will create the queue if 69 | // it does not already exist 70 | client, err := NewClient(NatsContext("WQ"), WorkQueue(queue), RetryBackoffPolicy(RetryLinearOneHour)) 71 | panicIfErr(err) 72 | 73 | router := NewTaskRouter() 74 | err = router.HandleFunc("email:send", func(_ context.Context, _ Logger, t *Task) (any, error) { 75 | log.Printf("Processing task: %s", t.ID) 76 | 77 | // handle task.Payload which is a JSON encoded email 78 | 79 | // task record will be updated with this payload result 80 | return "success", nil 81 | }) 82 | panicIfErr(err) 83 | 84 | // Starts handling registered tasks, blocks until canceled 85 | err = client.Run(context.Background(), router) 86 | panicIfErr(err) 87 | } 88 | 89 | func ExampleClient_LoadTaskByID() { 90 | client, err := NewClient(NatsContext("WQ")) 91 | panicIfErr(err) 92 | 93 | task, err := client.LoadTaskByID("24ErgVol4ZjpoQ8FAima9R2jEHB") 94 | panicIfErr(err) 95 | 96 | fmt.Printf("Loaded task %s in state %s", task.ID, task.State) 97 | } 98 | -------------------------------------------------------------------------------- /docs/.hugo_build.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choria-io/asyncjobs/f1ba2250e3990bcac45d6cf6ba94f74366ad8be4/docs/.hugo_build.lock -------------------------------------------------------------------------------- /docs/archetypes/default.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "{{ replace .Name "-" " " | title }}" 3 | date: {{ .Date }} 4 | draft: true 5 | --- 6 | 7 | -------------------------------------------------------------------------------- /docs/config.toml: -------------------------------------------------------------------------------- 1 | baseURL = "https://choria-io.github.io/asyncjobs/" 2 | languageCode = "en-us" 3 | title = "Choria Async Jobs Documentation" 4 | theme = "hugo-theme-relearn" 5 | canonifyURLs = true 6 | 7 | [outputs] 8 | home = [ "HTML", "RSS", "JSON"] 9 | 10 | [params] 11 | author = "Choria Project" 12 | showVisitedLinks = false 13 | disableLanguageSwitchingButton = true 14 | themeVariant = "asyncjobs" 15 | disableInlineCopyToClipBoard = true 16 | alwaysopen = true 17 | 18 | [markup] 19 | [markup.highlight] 20 | # if set to `guessSyntax = true`, there will be no unstyled code even if no language 21 | # was given BUT mermaid code fences will not work anymore! So this is a mandatory 22 | # setting for your site 23 | guessSyntax = false 24 | style = "tango" 25 | 26 | [[menu.shortcuts]] 27 | name = " GitHub" 28 | identifier = "ds" 29 | url = "https://github.com/choria-io/asyncjobs" 30 | weight = 10 31 | 32 | [[menu.shortcuts]] 33 | name = " Download" 34 | url = "https://github.com/choria-io/asyncjobs/releases" 35 | weight = 11 36 | 37 | [[menu.shortcuts]] 38 | name = " Discussions" 39 | url = "https://github.com/choria-io/asyncjobs/discussions" 40 | weight = 12 41 | 42 | [[menu.shortcuts]] 43 | name = " Issues" 44 | url = "https://github.com/choria-io/asyncjobs/issues" 45 | weight = 13 46 | 47 | [[menu.shortcuts]] 48 | name = " #choria" 49 | url = "https://slack.puppet.com/" 50 | weight = 14 51 | -------------------------------------------------------------------------------- /docs/content/_index.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Choria Async Jobs" 3 | weight = 5 4 | +++ 5 | 6 | # Introduction 7 | 8 | This is an Asynchronous Job Queue system that relies on NATS JetStream for storage and general job life cycle management. It is compatible with any NATS JetStream based system like a private hosted JetStream, Choria Streams or a commercial SaaS. 9 | 10 | Each Task is stored in JetStream by a unique ID and Work Queue item is made referencing that Task. JetStream will handle dealing with scheduling, retries, acknowledgements and more of the Work Queue item. The stored Task will be updated during the lifecycle. 11 | 12 | Multiple processes can process jobs concurrently, thus job processing is both horizontally and vertically scalable. Job handlers are implemented in Go with one process hosting one or many handlers. Other languages can implement Job Handlers using NATS Request-Reply services. Per process concurrency and overall per-queue concurrency controls exist. 13 | 14 | ## Synopsis 15 | 16 | Tasks are published to Work Queues: 17 | 18 | ```go 19 | // establish a connection to the EMAIL work queue using a NATS context 20 | client, _ := asyncjobs.NewClient(asyncjobs.NatsConn(nc), asyncjobs.BindWorkQueue("EMAIL")) 21 | 22 | // create a task with the type 'email:new' and body from newEmail() 23 | task, _ := asyncjobs.NewTask("email:new", newEmail()) 24 | 25 | // store it in the Work Queue 26 | client.EnqueueTask(ctx, task) 27 | ``` 28 | 29 | Tasks are processes by horizontally and vertically scalable processes. Typically, a Handler handles one type of Task. We have Prometheus 30 | integration, concurrency and backoffs configured. 31 | 32 | ```go 33 | // establish a connection to the EMAIL work queue using a 34 | // NATS context, with concurrency, prometheus stats and backoff 35 | client, _ := asyncjobs.NewClient( 36 | asyncjobs.NatsContext("EMAIL"), 37 | asyncjobs.BindWorkQueue("EMAIL"), 38 | asyncjobs.ClientConcurrency(10), 39 | asyncjobs.PrometheusListenPort(8080), 40 | asyncjobs.RetryBackoffPolicy(asyncjobs.RetryLinearTenMinutes)) 41 | 42 | router := asyncjobs.NewTaskRouter() 43 | router.Handler("email:new", func(ctx context.Context, log asyncjobs.Logger, task *asyncjobs.Task) (any, error) { 44 | log.Printf("Processing task %s", task.ID) 45 | 46 | // do work here using task.Payload 47 | 48 | return "sent", nil 49 | }) 50 | 51 | client.Run(ctx, router) 52 | ``` 53 | -------------------------------------------------------------------------------- /docs/content/overview/features.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Feature List" 3 | toc = false 4 | weight = 50 5 | +++ 6 | 7 | This feature list is incomplete, at present the focus is on determining what will work well for the particular patterns 8 | JetStream enables, so there might be some churn in the feature set here. 9 | 10 | ### Tasks 11 | 12 | * Task definitions stored post-processing, with various retention and discard policies 13 | * Ability to retry a Task that has already been completed or failed 14 | * Task deduplication 15 | * Deadline per task - after this time the task will not be processed 16 | * Tasks can depend on other tasks 17 | * Max tries per task, capped to the queue tries 18 | * Task state tracked throughout it's lifecycle 19 | * [K-Sortable](https://github.com/segmentio/ksuid) Task GUIDs 20 | * Lifecycle events published about [changes to task states](../../reference/lifecycle-events/) 21 | 22 | See [Task Lifecycle](../../reference/task-lifecycle/) for full background and details 23 | 24 | ### Queues 25 | 26 | * Queues can store different types of task 27 | * Queues with caps on queued items and different queue-full behaviors 28 | * Default or user supplied queue definitions 29 | * Queue per client, many clients per queue 30 | 31 | ### Processing 32 | 33 | * Retries of failed tasks with backoff schedules configurable using `RetryBackoffPolicy()`. Handler opt-in early termination. 34 | * Parallel processing of tasks, horizontally or vertically scaled. Run time adjustable upper boundary on a per-queue basis 35 | * Worker crashes does not impact the work queue 36 | * Handler interface with task router to select appropriate handler by task type with wildcard matches 37 | * Support for Handlers in all NATS Supported languages using [Remote Handlers](../../reference/request-reply/) 38 | * Statistics via Prometheus 39 | 40 | ### Storage 41 | 42 | * Replicated storage using RAFT protocol within JetStream Streams, disk based or memory based 43 | * KV for configuration and schedule storage 44 | * KV for leader elections 45 | 46 | ### Scheduled Tasks 47 | 48 | * Cron like schedules creating tasks on demand 49 | * HA capable Scheduler process integrated with `ajc` 50 | * Prometheus monitoring 51 | * CLI CRUD operations via `ajc task cron` 52 | 53 | See [Scheduled Tasks](../scheduled-tasks/) 54 | 55 | ### Misc 56 | 57 | * Supports NATS Contexts for connection configuration 58 | * Supports custom loggers, defaulting to go internal `log` 59 | 60 | ### Command Line 61 | 62 | * Various info and state requests 63 | * Configure aspects of Task and Queue storage 64 | * Watch task processing 65 | * Process tasks via shell commands 66 | * CRUD on Tasks store or individual Task 67 | * CRUD on Scheduled Tasks 68 | 69 | ## Planned Features 70 | 71 | A number of features are planned in the near term, see our [GitHub Issues](https://github.com/choria-io/asyncjobs/labels/enhancement) 72 | -------------------------------------------------------------------------------- /docs/content/overview/handlers-k8s.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Handlers in K8s" 3 | toc = true 4 | weight = 40 5 | +++ 6 | 7 | We publish Helm charts to deploy the system to Kubernetes. 8 | 9 | ## Requirements 10 | 11 | ### NATS Server with JetStream 12 | 13 | You need a NATS JetStream server, if you are a Choria User you can enable [Choria Streams](https://choria.io/docs/streams/) otherwise the NATS Community has their own [NATS Helm Charts](https://choria.io/docs/streams/). 14 | 15 | ### Connection Context 16 | 17 | We use NATS Contexts to configure the connection between asyncjobs and NATS. If you already have a context configured using the [NATS CLI](https://github.com/nats-io/natscli) then use `nats context show CONTEXTNAME --json` to get the keys and values to configure. 18 | 19 | For me I needed some TLS Certificates to authenticate to NATS along with the context, so we made a secret called `task-scheduler-tls` holding that, you can put NATS credential files and more in the same manner: 20 | 21 | ``` 22 | $ find asyncjobs/task-scheduler 23 | asyncjobs/task-scheduler/secret 24 | asyncjobs/task-scheduler/secret/tls.crt 25 | asyncjobs/task-scheduler/secret/tls.key 26 | asyncjobs/task-scheduler/secret/ca.crt 27 | $ kubectl -n asyncjobs create secret generic task-scheduler-tls --from-file asyncjobs/task-scheduler/secret 28 | ``` 29 | 30 | ### Choria Helm Repository 31 | 32 | Choria has it's own Helm repository that you need to import: 33 | 34 | ``` 35 | $ helm repo add choria https://choria-io.github.io/helm 36 | $ helm repo update 37 | ``` 38 | 39 | ### Kubernetes Namespace 40 | 41 | We suggest running the asyncjobs components in a namespace: 42 | 43 | ``` 44 | $ kubectl create namespace asyncjobs 45 | namespace/asyncjobs created 46 | ``` 47 | 48 | ## Task Scheduler 49 | 50 | Here I show a basic values file for the Task Scheduler, it will run 2 replicas with one being active: 51 | 52 | ```yaml 53 | # asyncjobs-task-scheduler-values.yaml 54 | image: 55 | tag: 0.0.6 56 | 57 | taskScheduler: 58 | contextSecret: task-scheduler-tls 59 | context: 60 | url: nats://broker-broker-ss:4222 61 | ca: /etc/asyncjobs/secret/ca.crt 62 | key: /etc/asyncjobs/secret/tls.key 63 | cert: /etc/asyncjobs/secret/tls.crt 64 | ``` 65 | 66 | We reference the secret added earlier. 67 | 68 | ``` 69 | $ helm install --namespace asyncjobs --values asyncjobs-task-scheduler-values.yaml task-scheduler choria/asyncjobs-task-scheduler 70 | ``` 71 | -------------------------------------------------------------------------------- /docs/content/reference/lifecycle-events.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Lifecycle Events" 3 | toc = true 4 | weight = 40 5 | +++ 6 | 7 | Lifecycle events are small JSON messages that are published to notify about various stages of processing and the life of a client. 8 | 9 | Today the only event we support is one notifying about changes in Task State but more will be added. In future we will support emitting Cloud Event standard events. 10 | 11 | Events are not guaranteed to be delivered and are not persisted, they are informational. While you can use them to build a kind of coupled system of waiting for a task to complete you should not rely on these events to be delivered in 100% of cases. 12 | 13 | ## Event Types 14 | 15 | Each event has a type like `io.choria.asyncjobs.v1.task_state` aka `asyncjobs.TaskStateChangeEventType` that can help with parsing and routing of events through other systems. 16 | 17 | ## Parsing an event 18 | 19 | We provide a helper to parse any supported event and to process them using the common go type switch pattern. 20 | 21 | ```go 22 | // subscribe to all events 23 | sub, err := nc.SubscribeSync(asyncjobs.EventsSubjectWildcard) 24 | panicIfErr(err) 25 | 26 | for { 27 | msg, _ := sub.NextMsg(time.Minute) 28 | event, kind, _ := asyncjobs.ParseEventJSON(msg.Data) 29 | 30 | switch e := event.(type) { 31 | case asyncjobs.TaskStateChangeEvent: 32 | // handle task state change event 33 | 34 | default: 35 | // logs the io.choria.asyncjobs.v1.task_state style task type 36 | log.Printf("Unknown event type %s", kind) 37 | } 38 | } 39 | ``` 40 | 41 | ## `TaskStateChangeEvent` 42 | 43 | This event type is published for any state change of a Task, using it you can watch a task by ID or all tasks. 44 | 45 | These events are published to `CHORIA_AJ.E.task_state.*` with the last token being the Job ID. 46 | 47 | On the wire the messages look like here, with `task_age` being a go Duration. 48 | 49 | ```json 50 | { 51 | "event_id": "24mHmiRY9eQCVU4xuHwsztJ2MJH", 52 | "type": "io.choria.asyncjobs.v1.task_state", 53 | "timestamp": "2022-02-07T10:16:42Z", 54 | "task_id": "24mHmkobHqLE6bxiWPTwuV30xrO", 55 | "state": "complete", 56 | "tries": 1, 57 | "queue": "DEFAULT", 58 | "task_type": "email:new", 59 | "task_age": 4037478 60 | } 61 | ``` 62 | -------------------------------------------------------------------------------- /docs/content/reference/request-reply.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Request-Reply Handlers" 3 | toc = true 4 | weight = 30 5 | +++ 6 | 7 | Typically, and for best performance, you implement your handlers in Go and compile them into the binary. 8 | 9 | In order to support other programming languages we also support calling out over NATS in a [Request-Reply](https://docs.nats.io/nats-concepts/core-nats/reqreply) fashion to a service that can be programmed in any language. 10 | 11 | It's worth understanding [Routing, Handlers, Concurrency and Retry](../routing-concurrency-retry/) for background, these remote callout Handlers map exactly onto that model. 12 | 13 | ## Registering with the Router 14 | 15 | ```go 16 | client, _ := asyncjobs.NewClient(asyncjobs.NatsConn(nc), asyncjobs.BindWorkQueue("EMAIL")) 17 | 18 | router := asyncjobs.NewTaskRouter() 19 | router.RequestReply("email:new", client) 20 | 21 | client.Run(router) 22 | ``` 23 | 24 | Here we register with the Router for tasks of type `email:new` that will call out via Request-Reply. 25 | 26 | If all your Handlers are of this type I strongly suggest investigating our [Docker Based Runner](../overview/handlers-docker/) that can achieve this without writing any Go code. 27 | 28 | ## Protocol 29 | 30 | We implement a light-weight JSON + Headers protocol to communicate with remote services. They support returning errors including the `ErrTerminateTask` behavior. 31 | 32 | Your service must listen on `CHORIA_AJ.H.T.email:new` - most probably in a queue group - where you would replace `email:new` with whatever you chose as a task type. A handler that is registered with task type `""` will handle all tasks of all types and the handling service should listen on `CHORIA.AJ.H.T.catchall`. 33 | 34 | ### Tasks 35 | 36 | A request for a Task Handler will have these headers: 37 | 38 | | Header | Value | 39 | |-----------------------|-------------------------------------| 40 | | `AJ-Content-Type` | `application/x-asyncjobs-task+json` | 41 | | `AJ-Handler-Deadline` | `2009-11-10T23:00:00Z` | 42 | 43 | The content-type is same for all Tasks, the Deadline is a UTC timestamp indicating by what time the remote service has to complete handling the task to avoid timeouts. 44 | 45 | The body is simply a JSON format `Task`. 46 | 47 | Responses from your service can have these headers: 48 | 49 | | Header | Description | 50 | |----------------|--------------------------------------------------------------------------------------------------------------------------------| 51 | | `AJ-Error` | Indicates an error was encountered, the value is set as task error, the task is retried later | 52 | | `AJ-Terminate` | Terminates the task via `ErrTerminateTask`, the value will be set as additional text in the error. No further retries are done | 53 | 54 | The body of your response is taken and stored with the Task unmodified. 55 | 56 | ## Demonstration 57 | 58 | To see this in action, we can use the `nats` CLI tool. 59 | 60 | ``` 61 | $ nats reply CHORIA_AJ.H.T.email:new 'success' --context AJC 62 | 18:33:32 [#1] Received on subject "CHORIA_AJ.H.T.email:new": 63 | 18:33:33 AJ-Content-Type: application/x-asyncjobs-task+json 64 | 18:33:33 AJ-Handler-Deadline: 2022-02-09T17:34:31Z 65 | 66 | {"id":"24smZHaWnjuP371iglxeQWK7nOi","type":"email:new","queue":"DEFAULT","payload":"InsuLi4ufSI=","state":"active","created":"2022-02-09T17:28:41.943198067Z","tried":"2022-02-09T17:33:33.005041134Z","tries":5} 67 | ``` 68 | 69 | The CLI received the jobs with the 2 headers set and appropriate payload, it responsed with `success` and the task was completed. 70 | 71 | ``` 72 | $ ajc task view 24smZHaWnjuP371iglxeQWK7nOi --json 73 | { 74 | "id": "24smZHaWnjuP371iglxeQWK7nOi", 75 | "type": "email:new", 76 | "queue": "DEFAULT", 77 | "payload": "InsuLi4ufSI=", 78 | "result": { 79 | "payload": "dGVzdA==", 80 | "completed": "2022-02-09T17:33:33.00755251Z" 81 | }, 82 | "state": "complete", 83 | "created": "2022-02-09T17:28:41.943198067Z", 84 | "tried": "2022-02-09T17:33:33.007552104Z", 85 | "tries": 5 86 | } 87 | ``` 88 | -------------------------------------------------------------------------------- /docs/content/reference/security.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Security" 3 | toc = true 4 | weight = 50 5 | +++ 6 | 7 | Sometimes you want to run a handler in a insecure location and want to be sure it only executes tasks from trusted creators. 8 | 9 | Tasks can be signed using ed25519 private keys and clients can be configured to only accept tasks created and signed using 10 | a specific key. We support requiring all tasks are signed when keys are configured (the default), or accepting unsigned tasks 11 | but requiring signed tasks are verified. 12 | 13 | First we need to create some keys, these should be saved to a file encoded using `hex.Encode()`. 14 | 15 | ```go 16 | pubk, prik, err = ed25519.GenerateKey(nil) 17 | panicIfErr(err) 18 | ``` 19 | 20 | Then we can configure the client: 21 | 22 | ```go 23 | client, err := asyncjobs.NewClient( 24 | asyncjobs.NatsContext("AJC"), 25 | 26 | // when tasks are created sign using this ed25519.PrivateKey, see also TaskSigningSeedFile() 27 | asyncjobs.TaskSigningKey(prik), 28 | 29 | // when loading tasks verify using this ed25519.PublicKey, see also TaskVerificationKeyFile() 30 | asyncjobs.TaskVerificationKey(pubk), 31 | 32 | // support loading unsigned tasks when a verification method is set, disabled by default 33 | asyncjobs.TaskSignaturesOptional(), 34 | ) 35 | panicIfErr(err) 36 | ``` 37 | 38 | On the command line the `ajc tasks` command has `--sign` and `--verify` flags which can either be hex encoded keys 39 | or paths to files holding them in hex encoded format. 40 | 41 | Docker containers built using `ajc package docker` can set a key in the environment variable `AJ_VERIFICATION_KEY` and 42 | can opt into optional signatures at build time by setting `task_signatures_optional: true` in the `asyncjobs.yaml`. -------------------------------------------------------------------------------- /docs/content/reference/terminology.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Terminology" 3 | toc = true 4 | weight = 60 5 | +++ 6 | 7 | Several terms are used in this system as outlined here. 8 | 9 | ## JetStream 10 | 11 | The underlying storage and work queue manager. See the [NATS project documentation](https://docs.nats.io/nats-concepts/jetstream) for background. 12 | 13 | ## Work Queue 14 | 15 | A Work Queue is JetStream Stream set with `WorkQueue` Retention policy. The underlying Stream holding these queues are called `CHORIA_AJ_Q_DEFAULT` for the `DEFAULT` queue. 16 | 17 | ## Work Item 18 | 19 | Work Items are placed in the Work Queue and scheduled by JetStream. The contents of the Work Queue are `ProcessItem` messages encoded in JSON format. 20 | 21 | ## Client 22 | 23 | Connects to JetStream and manages the enqueueing and routing of tasks. 24 | 25 | ## Handler 26 | 27 | Handlers are functions that can process a task with the signature `func(context.Context, *asyncjobs.Task) (any, error)`. 28 | 29 | ## Router 30 | 31 | The Router locates handlers for a particular task using the `Type` field as a matcher. 32 | 33 | See [Routing, Handlers, Concurrency and Retry](../routing-concurrency-retry/). 34 | 35 | ## Task 36 | 37 | A task is a specific kind of Work Item that is handled by a Handler via a Router, this is the main processible unit. In time we anticipate other kinds of Item for example Scheduled items, now the only kind of Item is a Task. 38 | 39 | Task have time stamps, statuses and more. See [Task Lifecycle](../task-lifecycle/). 40 | 41 | ## Lifecycle Event 42 | 43 | Events are small messages published to notify listeners about the state of changes. Today only Task State changes are reported, in future we will report more such as Processor start and stop etc. 44 | 45 | See [Lifecycle Events](../lifecycle-events/) 46 | 47 | ## Scheduled Task 48 | 49 | A cron like schedule for creating tasks on demand. Requires the running of a Task Scheduler process. See [Scheduled Tasks](../../overview/scheduled-tasks/) 50 | -------------------------------------------------------------------------------- /docs/layouts/partials/logo.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/static/css/theme-asyncjobs.css: -------------------------------------------------------------------------------- 1 | /* my-custom-variant */ 2 | :root { 3 | --SECONDARY-color: #29395B; /* brand secondary color */ 4 | --MAIN-TEXT-color: #323232; /* text color of content and h1 titles */ 5 | --MAIN-LINK-color: #67C287; /* link color of content */ 6 | --MAIN-LINK-HOVER-color: #67C287; /* hoverd link color of content */ 7 | --MAIN-ANCHOR-color: #67C287; /* anchor color of titles */ 8 | --MAIN-BG-color: #ffffff; /* background color of content */ 9 | --MAIN-TITLES-TEXT-color: #5e5e5e; /* text color of h2-h6 titles and transparent box titles */ 10 | --CODE-BLOCK-color: #e2e4e5; /* fallback text color of block code; should be adjusted to your selected chroma style */ 11 | --CODE-BLOCK-BG-color: #282a36; /* fallback background color of block code; should be adjusted to your selected chroma style */ 12 | --CODE-BLOCK-BORDER-color: #282a36; /* border color of block code */ 13 | --CODE-INLINE-color: #5e5e5e; /* text color of inline code */ 14 | --CODE-INLINE-BG-color: #fff7dd; /* background color of inline code */ 15 | --CODE-INLINE-BORDER-color: #fbf0cb; /* border color of inline code */ 16 | --MENU-HEADER-BG-color: #666666; /* background color of menu header */ 17 | --MENU-HEADER-BORDER-color: #67C287; /* separator color of menu header */ 18 | --MENU-HOME-LINK-color: #29395B; /* home button color if configured */ 19 | --MENU-HOME-LINK-HOVER-color: #29395B; /* hoverd home button color if configured */ 20 | --MENU-SEARCH-color: #ffffff; /* text and icon color of search box */ 21 | --MENU-SEARCH-BG-color: #333333; /* background color of search box */ 22 | --MENU-SEARCH-BORDER-color: #29395B; /* border color of search box */ 23 | --MENU-SECTIONS-BG-color: #322a38; /* background of the menu; this is NOT just a color value but can be a complete CSS background definition including gradients, etc. */ 24 | --MENU-SECTIONS-ACTIVE-BG-color: #251f29; /* background color of the active menu section */ 25 | --MENU-SECTIONS-LINK-color: #cccccc; /* link color of menu topics */ 26 | --MENU-SECTIONS-LINK-HOVER-color: #e6e6e6; /* hoverd link color of menu topics */ 27 | --MENU-SECTION-ACTIVE-CATEGORY-color: #777777; /* text color of the displayed menu topic */ 28 | --MENU-SECTION-ACTIVE-CATEGORY-BG-color: #ffffff; /* background color of the displayed menu topic */ 29 | --MENU-SECTION-HR-color: #2a232f; /* separator color of menu footer */ 30 | --MENU-VISITED-color: #29395B; /* icon color of visited menu topics if configured */ 31 | --BOX-CAPTION-color: rgba( 255, 255, 255, 1 ); /* text color of colored box titles */ 32 | --BOX-BG-color: rgba( 255, 255, 255, .833 ); /* background color of colored boxes */ 33 | --BOX-TEXT-color: rgba( 16, 16, 16, 1 ); /* text color of colored box content */ 34 | } 35 | -------------------------------------------------------------------------------- /docs/static/css/theme-my-custom-variant.css: -------------------------------------------------------------------------------- 1 | /* my-custom-variant */ 2 | :root { 3 | --BOX-BG-color: rgba( 20, 20, 20, 1 ); /* background color of colored boxes */ 4 | --BOX-CAPTION-color: rgba( 240, 240, 240, 1 ); /* text color of colored box titles */ 5 | --BOX-TEXT-color: #e0e0e0; /* text color of colored box content */ 6 | --CODE-BLOCK-BG-color: #2b2b2b; /* fallback background color of block code; should be adjusted to your selected chroma style */ 7 | --CODE-BLOCK-color: #f8f8f8; /* fallback text color of block code; should be adjusted to your selected chroma style */ 8 | --CODE-INLINE-BG-color: #2d2d2d; /* background color of inline code */ 9 | --CODE-INLINE-BORDER-color: #464646; /* border color of inline code */ 10 | --CODE-INLINE-color: #E39B4D; /* text color of inline code */ 11 | --MAIN-BG-color: #202020; /* background color of content */ 12 | --MAIN-LINK-color: #E39B4D; /* link color of content */ 13 | --MAIN-LINK-HOVER-color: #4cabff; /* hoverd link color of content */ 14 | --MAIN-TEXT-color: #e0e0e0; /* text color of content and h1 titles */ 15 | --MAIN-TITLES-TEXT-color: #ffffff; /* text color of h2-h6 titles and transparent box titles */ 16 | --MENU-HEADER-BG-color: #555555; /* background color of menu header */ 17 | --MENU-HEADER-BORDER-color: #E39B4D; /* separator color of menu header */ 18 | --MENU-HOME-LINK-HOVER-color: #5e5e5e; /* hoverd home button color if configured */ 19 | --MENU-SEARCH-BORDER-color: #e0e0e0; /* border color of search box */ 20 | --MENU-SECTION-ACTIVE-CATEGORY-color: #E39B4D; /* text color of the displayed menu topic */ 21 | --MENU-SECTIONS-ACTIVE-BG-color: #323232; /* background color of the active menu section */ 22 | --MENU-SECTIONS-BG-color: #2b2b2b; /* background of the menu; this is NOT just a color value but can be a complete CSS background definition including gradients, etc. */ 23 | --MENU-SECTIONS-LINK-HOVER-color: #ffffff; /* hoverd link color of menu topics */ 24 | --MENU-VISITED-color: #E39B4D; /* icon color of visited menu topics if configured */ 25 | --MERMAID-theme: dark; /* name of the default Mermaid theme for this variant, can be overridden in config.toml */ 26 | --SWAGGER-theme: dark; /* name of the default Swagger theme for this variant, can be overridden in config.toml */ 27 | } 28 | -------------------------------------------------------------------------------- /docs/static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choria-io/asyncjobs/f1ba2250e3990bcac45d6cf6ba94f74366ad8be4/docs/static/logo.png -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 2 9 | indent_style = space 10 | trim_trailing_whitespace = true 11 | 12 | [*.css] 13 | indent_size = 4 14 | 15 | [*.js] 16 | insert_final_newline = true 17 | 18 | [*.md] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | public/ 3 | exampleSite/public 4 | exampleSite/hugo*.exe 5 | .hugo_build.lock 6 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/.grenrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | changelogFilename: "exampleSite/content/basics/CHANGELOG.md", 3 | dataSource: "milestones", 4 | groupBy: { 5 | "Enhancements": [ 6 | "feature", 7 | ], 8 | "Fixes": [ 9 | "bug" 10 | ], 11 | "Maintenance": [ 12 | "task", 13 | ], 14 | "Uncategorised": [ 15 | "closed", 16 | ], 17 | }, 18 | ignoreLabels: [ 19 | "hugo", 20 | ], 21 | ignoreIssuesWith: [ 22 | "discussion", 23 | "documentation", 24 | "duplicate", 25 | "invalid", 26 | "support", 27 | "wontfix", 28 | ], 29 | ignoreTagsWith: [ 30 | "Relearn", 31 | ], 32 | milestoneMatch: "{{tag_name}}", 33 | onlyMilestones: true, 34 | template: { 35 | group: "\n### {{heading}}\n", 36 | release: ({ body, date, release }) => `## ${release} (` + date.replace( /(\d+)\/(\d+)\/(\d+)/, '$3-$2-$1' ) + `)\n${body}`, 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/.issuetracker: -------------------------------------------------------------------------------- 1 | # Integration with Issue Tracker 2 | # 3 | # (note that '\' need to be escaped). 4 | 5 | [issuetracker "GitHub Rule"] 6 | regex = "#(\\d+)" 7 | url = "https://github.com/McShelby/hugo-theme-relearn/issues/$1" 8 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Sören Weber 4 | Copyright (c) 2017 Valere JEANTET 5 | Copyright (c) 2016 MATHIEU CORNIC 6 | Copyright (c) 2014 Grav 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy of 9 | this software and associated documentation files (the "Software"), to deal in 10 | the Software without restriction, including without limitation the rights to 11 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 12 | the Software, and to permit persons to whom the Software is furnished to do so, 13 | subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 20 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 21 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 22 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 23 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/README.md: -------------------------------------------------------------------------------- 1 | # Hugo Relearn Theme 2 | 3 | A theme for [Hugo](https://gohugo.io/) designed for documentation. 4 | 5 | ![Overview](https://github.com/McShelby/hugo-theme-relearn/raw/main/images/screenshot.png) 6 | 7 | ## Motivation 8 | 9 | The theme is a fork of the great [Learn theme](https://github.com/matcornic/hugo-theme-learn) with the aim of fixing long outstanding bugs and adepting to latest Hugo features. As far as possible this theme tries to be a drop-in replacement for the Learn theme. 10 | 11 | ## Main features 12 | 13 | - Usable offline, no external dependencies 14 | - Automatic Search 15 | - Print whole chapters or even the complete site 16 | - Multilingual mode for English, Arabic, Simplified Chinese, Traditional Chinesse, Dutch, French, German, Hindi, Indonesian, Japanese, Korean, Portuguese, Russian, Spanish, Turkish, Vietnamese 17 | - Unlimited menu levels 18 | - Automatic next/prev buttons to navigate through menu entries 19 | - Image resizing, shadow… 20 | - Attachments files 21 | - List child pages 22 | - Mermaid diagram (flowchart, sequence, gantt) 23 | - Swagger UI for OpenAPI Specifications 24 | - Customizable look and feel 25 | - Predefined (light, dark) and customizable color variants 26 | - Buttons 27 | - Tip/Note/Info/Warning boxes 28 | - Expand 29 | - Tabs 30 | - File inclusion 31 | ## Installation 32 | 33 | Visit the [installation instructions](https://mcshelby.github.io/hugo-theme-relearn/basics/installation) to learn how to setup the theme in your Hugo installation. 34 | 35 | ## Usage 36 | 37 | Visit the [documentation](https://mcshelby.github.io/hugo-theme-relearn/) to learn about all available features and how to use them. 38 | 39 | ## Changelog 40 | 41 | See the [changelog](https://mcshelby.github.io/hugo-theme-relearn/basics/history) for a complete list of releases. 42 | 43 | ## Contribution 44 | 45 | You are most welcome to contribute to the source code but please visit the [contribution guidelines](https://github.com/McShelby/hugo-theme-relearn/blob/main/.github/contributing.md) first. 46 | 47 | ## License 48 | 49 | This theme is licensed under the [MIT License](https://github.com/McShelby/hugo-theme-relearn/blob/main/LICENSE). 50 | 51 | ## Credits 52 | 53 | Special thanks to [everyone who has contributed](https://github.com/McShelby/hugo-theme-relearn/graphs/contributors) to this project. 54 | 55 | Many thanks to [Mathieu Cornic](https://github.com/matcornic) for his work on porting the [Learn theme](https://github.com/matcornic/hugo-theme-learn) to Hugo. 56 | 57 | Many thanks to [Andy Miller](https://github.com/rhukster) for initially creating the [Learn theme](https://github.com/getgrav/grav-theme-learn2) for Grav. 58 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/archetypes/chapter.md: -------------------------------------------------------------------------------- 1 | +++ 2 | chapter = true 3 | pre = "X. " 4 | title = "{{ replace .Name "-" " " | title }}" 5 | weight = 5 6 | +++ 7 | 8 | ### Chapter X 9 | 10 | # Some Chapter title 11 | 12 | Lorem Ipsum. -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/archetypes/default.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "{{ replace .Name "-" " " | title }}" 3 | weight = 5 4 | +++ 5 | 6 | Lorem Ipsum. -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/config.toml: -------------------------------------------------------------------------------- 1 | [module] 2 | [module.hugoVersion] 3 | min = "0.93.0" 4 | 5 | [outputFormats] 6 | [outputFormats.PRINT] 7 | name= "PRINT" 8 | baseName = "index" 9 | path = "_print" 10 | isHTML = true 11 | mediaType = 'text/html' 12 | permalinkable = false 13 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/i18n/ar.toml: -------------------------------------------------------------------------------- 1 | [Search-placeholder] 2 | other = "...البحث" 3 | 4 | [Clear-History] 5 | other = "مسح السجل" 6 | 7 | [Attachments-label] 8 | other = "مرفقات" 9 | 10 | [title-404] 11 | other = "خطأ" 12 | 13 | [message-404] 14 | other = ".¯\\_(ツ)_/¯أوبس. يبدو أن هذه الصفحة غير موجودة" 15 | 16 | [Go-to-homepage] 17 | other = "الذهاب إلى الصفحة الرئيسية" 18 | 19 | [Edit-this-page] 20 | other = "حرر" 21 | 22 | [Print-this-chapter] 23 | other = "طباعة الفصل بأكمله" 24 | 25 | [Shortcuts-Title] 26 | other = "المزيد" 27 | 28 | [Expand-title] 29 | other = "...قم بتوسيع" 30 | 31 | [Navigation-toggle] 32 | other = "قائمة" 33 | 34 | [Toc-toggle] 35 | other = "جدول المحتويات" 36 | 37 | [Byte-symbol] 38 | other = "B" 39 | 40 | [Kilobyte-symbol] 41 | other = "KB" 42 | 43 | [Megabyte-symbol] 44 | other = "MB" 45 | 46 | [note] 47 | other = "ملاحظه" 48 | 49 | [info] 50 | other = "معلومات" 51 | 52 | [tip] 53 | other = "بقشيش" 54 | 55 | [warning] 56 | other = "تحذير" 57 | 58 | [Copy-to-clipboard] 59 | other = "نسخ إلى الحافظة" 60 | 61 | [Copied-to-clipboard] 62 | other = "نسخت إلى الحافظة!" 63 | 64 | [Copy-link-to-clipboard] 65 | other = "نسخ الرابط إلى الحافظة" 66 | 67 | [Link-copied-to-clipboard] 68 | other = "رابط نسخت إلى الحافظة!" 69 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/i18n/de.toml: -------------------------------------------------------------------------------- 1 | [Search-placeholder] 2 | other = "Suchen..." 3 | 4 | [Clear-History] 5 | other = "Verlauf löschen" 6 | 7 | [Attachments-label] 8 | other = "Anhänge" 9 | 10 | [title-404] 11 | other = "Fehler" 12 | 13 | [message-404] 14 | other = "Huch. Diese Seite scheint nicht zu existieren ¯\\_(ツ)_/¯." 15 | 16 | [Go-to-homepage] 17 | other = "Gehe zur Homepage" 18 | 19 | [Edit-this-page] 20 | other = "Bearbeiten" 21 | 22 | [Print-this-chapter] 23 | other = "Ganzes Kapitel drucken" 24 | 25 | [Shortcuts-Title] 26 | other = "Mehr" 27 | 28 | [Expand-title] 29 | other = "Erweitere mich..." 30 | 31 | [Navigation-toggle] 32 | other = "Menu" 33 | 34 | [Toc-toggle] 35 | other = "Inhalt" 36 | 37 | [Byte-symbol] 38 | other = "B" 39 | 40 | [Kilobyte-symbol] 41 | other = "KB" 42 | 43 | [Megabyte-symbol] 44 | other = "MB" 45 | 46 | [note] 47 | other = "Anmerkung" 48 | 49 | [info] 50 | other = "Info" 51 | 52 | [tip] 53 | other = "Tipp" 54 | 55 | [warning] 56 | other = "Warnung" 57 | 58 | [Copy-to-clipboard] 59 | other = "In Zwischenablage kopieren" 60 | 61 | [Copied-to-clipboard] 62 | other = "In Zwischenablage kopiert!" 63 | 64 | [Copy-link-to-clipboard] 65 | other = "Link in Zwischenablage kopieren" 66 | 67 | [Link-copied-to-clipboard] 68 | other = "Link in Zwischenablage kopiert!" 69 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/i18n/en.toml: -------------------------------------------------------------------------------- 1 | [Search-placeholder] 2 | other = "Search..." 3 | 4 | [Clear-History] 5 | other = "Clear History" 6 | 7 | [Attachments-label] 8 | other = "Attachments" 9 | 10 | [title-404] 11 | other = "Error" 12 | 13 | [message-404] 14 | other = "Woops. Looks like this page doesn't exist ¯\\_(ツ)_/¯." 15 | 16 | [Go-to-homepage] 17 | other = "Go to homepage" 18 | 19 | [Edit-this-page] 20 | other = "Edit" 21 | 22 | [Print-this-chapter] 23 | other = "Print whole chapter" 24 | 25 | [Shortcuts-Title] 26 | other = "More" 27 | 28 | [Expand-title] 29 | other = "Expand me..." 30 | 31 | [Navigation-toggle] 32 | other = "Menu" 33 | 34 | [Toc-toggle] 35 | other = "Table of Contents" 36 | 37 | [Byte-symbol] 38 | other = "B" 39 | 40 | [Kilobyte-symbol] 41 | other = "KB" 42 | 43 | [Megabyte-symbol] 44 | other = "MB" 45 | 46 | [note] 47 | other = "Note" 48 | 49 | [info] 50 | other = "Info" 51 | 52 | [tip] 53 | other = "Tip" 54 | 55 | [warning] 56 | other = "Warning" 57 | 58 | [Copy-to-clipboard] 59 | other = "Copy to clipboard" 60 | 61 | [Copied-to-clipboard] 62 | other = "Copied to clipboard!" 63 | 64 | [Copy-link-to-clipboard] 65 | other = "Copy link to clipboard" 66 | 67 | [Link-copied-to-clipboard] 68 | other = "Copied link to clipboard!" 69 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/i18n/es.toml: -------------------------------------------------------------------------------- 1 | [Search-placeholder] 2 | other = "Buscar..." 3 | 4 | [Clear-History] 5 | other = "Borrar Historial" 6 | 7 | [Attachments-label] 8 | other = "Adjuntos" 9 | 10 | [title-404] 11 | other = "Error" 12 | 13 | [message-404] 14 | other = "Ups. Parece que la página no existe ¯\\_(ツ)_/¯." 15 | 16 | [Go-to-homepage] 17 | other = "Ir al inicio" 18 | 19 | [Edit-this-page] 20 | other = "Editar" 21 | 22 | [Print-this-chapter] 23 | other = "Imprimer le chapitre entier" 24 | 25 | [Shortcuts-Title] 26 | other = "Más" 27 | 28 | [Expand-title] 29 | other = "Expandir..." 30 | 31 | [Navigation-toggle] 32 | other = "Menú" 33 | 34 | [Toc-toggle] 35 | other = "Tabla de contenido" 36 | 37 | [Byte-symbol] 38 | other = "B" 39 | 40 | [Kilobyte-symbol] 41 | other = "KB" 42 | 43 | [Megabyte-symbol] 44 | other = "MB" 45 | 46 | [note] 47 | other = "Nota" 48 | 49 | [info] 50 | other = "Información" 51 | 52 | [tip] 53 | other = "Consejo" 54 | 55 | [warning] 56 | other = "Aviso" 57 | 58 | [Copy-to-clipboard] 59 | other = "Copiar en el portapapeles" 60 | 61 | [Copied-to-clipboard] 62 | other = "¡Copiado al portapapeles!" 63 | 64 | [Copy-link-to-clipboard] 65 | other = "Copiar enlace al portapapeles" 66 | 67 | [Link-copied-to-clipboard] 68 | other = "¡Enlace copiado al portapapeles!" 69 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/i18n/fr.toml: -------------------------------------------------------------------------------- 1 | [Search-placeholder] 2 | other = "Rechercher..." 3 | 4 | [Clear-History] 5 | other = "Supprimer l'historique" 6 | 7 | [Attachments-label] 8 | other = "Pièces jointes" 9 | 10 | [title-404] 11 | other = "Erreur" 12 | 13 | [message-404] 14 | other = "Oups. On dirait que cette page n'existe pas ¯\\_(ツ)_/¯" 15 | 16 | [Go-to-homepage] 17 | other = "Vers la page d'accueil" 18 | 19 | [Edit-this-page] 20 | other = "Éditer" 21 | 22 | [Print-this-chapter] 23 | other = "Imprimer le chapitre entier" 24 | 25 | [Shortcuts-Title] 26 | other = "Aller plus loin" 27 | 28 | [Expand-title] 29 | other = "Déroulez-moi..." 30 | 31 | [Navigation-toggle] 32 | other = "Menu" 33 | 34 | [Toc-toggle] 35 | other = "Table des matières" 36 | 37 | [Byte-symbol] 38 | other = "o" 39 | 40 | [Kilobyte-symbol] 41 | other = "ko" 42 | 43 | [Megabyte-symbol] 44 | other = "Mo" 45 | 46 | [note] 47 | other = "Remarque" 48 | 49 | [info] 50 | other = "Information" 51 | 52 | [tip] 53 | other = "Astuce" 54 | 55 | [warning] 56 | other = "Avertissement" 57 | 58 | [Copy-to-clipboard] 59 | other = "Copier dans le presse-papiers" 60 | 61 | [Copied-to-clipboard] 62 | other = "Copié dans le presse-papiers!" 63 | 64 | [Copy-link-to-clipboard] 65 | other = "Copier le lien dans le presse-papiers" 66 | 67 | [Link-copied-to-clipboard] 68 | other = "Lien copié dans le presse-papiers!" 69 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/i18n/hi.toml: -------------------------------------------------------------------------------- 1 | [Search-placeholder] 2 | other = "खोजे..." 3 | 4 | [Clear-History] 5 | other = "इतिहास मिटाएँ" 6 | 7 | [Attachments-label] 8 | other = "संलग्नंक (अटैचमेंट)" 9 | 10 | [title-404] 11 | other = "त्रुटि" 12 | 13 | [message-404] 14 | other = "यह पृष्ठ अभि अनुपलब्ध है!" 15 | 16 | [Go-to-homepage] 17 | other = "मुख्य पृष्ठ पर जाऐ" 18 | 19 | [Edit-this-page] 20 | other = "संपादन करना" 21 | 22 | [Print-this-chapter] 23 | other = "पूरा अध्याय मुद्रित करें" 24 | 25 | [Shortcuts-Title] 26 | other = "अधिक सामग्री दिखाएं" 27 | 28 | [Expand-title] 29 | other = "विस्तार करे..." 30 | 31 | [Navigation-toggle] 32 | other = "Menú" 33 | 34 | [Toc-toggle] 35 | other = "विषयसूची" 36 | 37 | [Byte-symbol] 38 | other = "B" 39 | 40 | [Kilobyte-symbol] 41 | other = "KB" 42 | 43 | [Megabyte-symbol] 44 | other = "MB" 45 | 46 | [note] 47 | other = "नोट" 48 | 49 | [info] 50 | other = "जानकारी" 51 | 52 | [tip] 53 | other = "नोक" 54 | 55 | [warning] 56 | other = "चेतावनी" 57 | 58 | [Copy-to-clipboard] 59 | other = "क्लिपबोर्ड पर प्रतिलिपि बनाएँ" 60 | 61 | [Copied-to-clipboard] 62 | other = "क्लिपबोर्ड पर कॉपी किया गया!" 63 | 64 | [Copy-link-to-clipboard] 65 | other = "क्लिपबोर्ड पर लिंक की प्रतिलिपि बनाएँ" 66 | 67 | [Link-copied-to-clipboard] 68 | other = "लिंक क्लिपबोर्ड के लिए प्रतिलिपि बनाई!" 69 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/i18n/id.toml: -------------------------------------------------------------------------------- 1 | [Search-placeholder] 2 | other = "Telusuri..." 3 | 4 | [Clear-History] 5 | other = "Bersihkan Riwayat" 6 | 7 | [Attachments-label] 8 | other = "Lampiran" 9 | 10 | [title-404] 11 | other = "Kesalahan" 12 | 13 | [message-404] 14 | other = "Oops. Sepertinya halaman ini tidak ada ¯\\_(ツ)_/¯." 15 | 16 | [Go-to-homepage] 17 | other = "Ke halaman depan" 18 | 19 | [Edit-this-page] 20 | other = "Mengedit" 21 | 22 | [Print-this-chapter] 23 | other = "Mencetak seluruh bab" 24 | 25 | [Shortcuts-Title] 26 | other = "Lainnya" 27 | 28 | [Expand-title] 29 | other = "Bentangkan..." 30 | 31 | [Navigation-toggle] 32 | other = "Menu" 33 | 34 | [Toc-toggle] 35 | other = "Daftar isi" 36 | 37 | [Byte-symbol] 38 | other = "B" 39 | 40 | [Kilobyte-symbol] 41 | other = "KB" 42 | 43 | [Megabyte-symbol] 44 | other = "MB" 45 | 46 | [note] 47 | other = "Nota" 48 | 49 | [info] 50 | other = "Info" 51 | 52 | [tip] 53 | other = "Ujung" 54 | 55 | [warning] 56 | other = "Peringatan" 57 | 58 | [Copy-to-clipboard] 59 | other = "Salin ke clipboard" 60 | 61 | [Copied-to-clipboard] 62 | other = "Disalin ke clipboard!" 63 | 64 | [Copy-link-to-clipboard] 65 | other = "Salin link ke clipboard" 66 | 67 | [Link-copied-to-clipboard] 68 | other = "Link disalin ke clipboard!" 69 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/i18n/ja.toml: -------------------------------------------------------------------------------- 1 | [Search-placeholder] 2 | other = "検索..." 3 | 4 | [Clear-History] 5 | other = "履歴削除" 6 | 7 | [Attachments-label] 8 | other = "添付" 9 | 10 | [title-404] 11 | other = "エラー" 12 | 13 | [message-404] 14 | other = "おっと。ページが見当たりません。 ¯\\_(ツ)_/¯." 15 | 16 | [Go-to-homepage] 17 | other = "ホームページへ行く" 18 | 19 | [Edit-this-page] 20 | other = "編集" 21 | 22 | [Print-this-chapter] 23 | other = "章全体を印刷する" 24 | 25 | [Shortcuts-Title] 26 | other = "更に" 27 | 28 | [Expand-title] 29 | other = "開く..." 30 | 31 | [Navigation-toggle] 32 | other = "メニュー" 33 | 34 | [Toc-toggle] 35 | other = "目次" 36 | 37 | [Byte-symbol] 38 | other = "B" 39 | 40 | [Kilobyte-symbol] 41 | other = "KB" 42 | 43 | [Megabyte-symbol] 44 | other = "MB" 45 | 46 | [note] 47 | other = "手記" 48 | 49 | [info] 50 | other = "情報" 51 | 52 | [tip] 53 | other = "先端" 54 | 55 | [warning] 56 | other = "警告" 57 | 58 | [Copy-to-clipboard] 59 | other = "クリップボードにコピー" 60 | 61 | [Copied-to-clipboard] 62 | other = "クリップボードにコピー!" 63 | 64 | [Copy-link-to-clipboard] 65 | other = "リンクをクリップボードにコピー" 66 | 67 | [Link-copied-to-clipboard] 68 | other = "リンクをクリップボードにコピーしました!" 69 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/i18n/kr.toml: -------------------------------------------------------------------------------- 1 | [Search-placeholder] 2 | other = "검색어를 입력하세요." 3 | 4 | [Clear-History] 5 | other = "방문 기록 삭제" 6 | 7 | [Attachments-label] 8 | other = "첨부파일" 9 | 10 | [title-404] 11 | other = "오류" 12 | 13 | [message-404] 14 | other = "존재하지 않는 페이지입니다." 15 | 16 | [Go-to-homepage] 17 | other = "메인화면" 18 | 19 | [Edit-this-page] 20 | other = "편집" 21 | 22 | [Print-this-chapter] 23 | other = "전체 장 인쇄" 24 | 25 | [Shortcuts-Title] 26 | other = "외부 링크" 27 | 28 | [Expand-title] 29 | other = "더 보기" 30 | 31 | [Navigation-toggle] 32 | other = "메뉴" 33 | 34 | [Toc-toggle] 35 | other = "목차" 36 | 37 | [Byte-symbol] 38 | other = "B" 39 | 40 | [Kilobyte-symbol] 41 | other = "KB" 42 | 43 | [Megabyte-symbol] 44 | other = "MB" 45 | 46 | [note] 47 | # code snippent 내의 comment(주석)과의 혼동 방지를 위해 위와 같이 번역합니다. 48 | other = "주" 49 | 50 | [info] 51 | # 예제 내용상 learn theme 참고 용도로 사용되어 위와 같이 번역합니다. 52 | other = "참고" 53 | 54 | [tip] 55 | # 우리말 순화어로 기록 가능하고, note, info와 의미상 좀 더 명확한 구분이 되어 아래처럼 기록합니다. 56 | other = "도움말" 57 | 58 | [warning] 59 | other = "주의" 60 | 61 | [Copy-to-clipboard] 62 | other = "클립보드에 복사" 63 | 64 | [Copied-to-clipboard] 65 | other = "클립보드에 복사됐습니다!" 66 | 67 | [Copy-link-to-clipboard] 68 | other = "링크를 클립보드에 복사" 69 | 70 | [Link-copied-to-clipboard] 71 | other = "클립보드에 링크가 복사됐습니다!" 72 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/i18n/nl.toml: -------------------------------------------------------------------------------- 1 | [Search-placeholder] 2 | other = "Zoeken..." 3 | 4 | [Clear-History] 5 | other = "Wis geschiedenis" 6 | 7 | [Attachments-label] 8 | other = "Bijlagen" 9 | 10 | [title-404] 11 | other = "Error" 12 | 13 | [message-404] 14 | other = "Blijkbaar bestaat deze pagina niet ¯\\_(ツ)_/¯." 15 | 16 | [Go-to-homepage] 17 | other = "Naar startpagina" 18 | 19 | [Edit-this-page] 20 | other = "Bewerken" 21 | 22 | [Print-this-chapter] 23 | other = "Het hele hoofdstuk afdrukken" 24 | 25 | [Shortcuts-Title] 26 | other = "Snelkoppelingen" 27 | 28 | [Expand-title] 29 | other = "Lees meer..." 30 | 31 | [Navigation-toggle] 32 | other = "Menu" 33 | 34 | [Toc-toggle] 35 | other = "Inhoudsopgave" 36 | 37 | [Byte-symbol] 38 | other = "B" 39 | 40 | [Kilobyte-symbol] 41 | other = "KB" 42 | 43 | [Megabyte-symbol] 44 | other = "MB" 45 | 46 | [note] 47 | other = "Notitie" 48 | 49 | [info] 50 | other = "Info" 51 | 52 | [tip] 53 | other = "Fooi" 54 | 55 | [warning] 56 | other = "Waarschuwing" 57 | 58 | [Copy-to-clipboard] 59 | other = "Naar klembord kopiëren" 60 | 61 | [Copied-to-clipboard] 62 | other = "Gekopieerd naar klembord!" 63 | 64 | [Copy-link-to-clipboard] 65 | other = "Link naar klembord kopiëren" 66 | 67 | [Link-copied-to-clipboard] 68 | other = "Link gekopieerd naar klembord!" 69 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/i18n/pir.toml: -------------------------------------------------------------------------------- 1 | [Search-placeholder] 2 | other = "Searrrch..." 3 | 4 | [Clear-History] 5 | other = "Clear Historrry" 6 | 7 | [Attachments-label] 8 | other = "Attachments" 9 | 10 | [title-404] 11 | other = "Errror" 12 | 13 | [message-404] 14 | other = "Woops. Looks like this plank doesn't exist ¯\\_(ツ)_/¯." 15 | 16 | [Go-to-homepage] 17 | other = "Go t' homeplank" 18 | 19 | [Edit-this-page] 20 | other = "Edit" 21 | 22 | [Print-this-chapter] 23 | other = "Prrrint whole chapterrr" 24 | 25 | [Shortcuts-Title] 26 | other = "Morrre" 27 | 28 | [Expand-title] 29 | other = "Expand me..." 30 | 31 | [Navigation-toggle] 32 | other = "Menu" 33 | 34 | [Toc-toggle] 35 | other = "Table o' Contents" 36 | 37 | [Byte-symbol] 38 | other = "B" 39 | 40 | [Kilobyte-symbol] 41 | other = "KB" 42 | 43 | [Megabyte-symbol] 44 | other = "MB" 45 | 46 | [note] 47 | other = "Avast" 48 | 49 | [info] 50 | other = "Ahoi" 51 | 52 | [tip] 53 | other = "Smarrrt arrrse" 54 | 55 | [warning] 56 | other = "Arrr" 57 | 58 | [Copy-to-clipboard] 59 | other = "Copy t' clipboard" 60 | 61 | [Copied-to-clipboard] 62 | other = "Copied t' clipboard!" 63 | 64 | [Copy-link-to-clipboard] 65 | other = "Copy link t' clipboard" 66 | 67 | [Link-copied-to-clipboard] 68 | other = "Copied link t' clipboard!" 69 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/i18n/pt.toml: -------------------------------------------------------------------------------- 1 | [Search-placeholder] 2 | other = "Procurar..." 3 | 4 | [Clear-History] 5 | other = "Limpar Histórico" 6 | 7 | [Attachments-label] 8 | other = "Anexos" 9 | 10 | [title-404] 11 | other = "Erro" 12 | 13 | [message-404] 14 | other = "Ops. Parece que a página não existe ¯\\_(ツ)_/¯." 15 | 16 | [Go-to-homepage] 17 | other = "Ir para o início" 18 | 19 | [Edit-this-page] 20 | other = "Editar" 21 | 22 | [Print-this-chapter] 23 | other = "Imprimir capítulo inteiro" 24 | 25 | [Shortcuts-Title] 26 | other = "Mais" 27 | 28 | [Expand-title] 29 | other = "Expandir..." 30 | 31 | [Navigation-toggle] 32 | other = "Menu" 33 | 34 | [Toc-toggle] 35 | other = "Índice" 36 | 37 | [Byte-symbol] 38 | other = "B" 39 | 40 | [Kilobyte-symbol] 41 | other = "KB" 42 | 43 | [Megabyte-symbol] 44 | other = "MB" 45 | 46 | [note] 47 | other = "Nota" 48 | 49 | [info] 50 | other = "Informação" 51 | 52 | [tip] 53 | other = "Dica" 54 | 55 | [warning] 56 | other = "Aviso" 57 | 58 | [Copy-to-clipboard] 59 | other = "Copiar para a área de transferência" 60 | 61 | [Copied-to-clipboard] 62 | other = "Copiado para a área de transferência!" 63 | 64 | [Copy-link-to-clipboard] 65 | other = "Link de cópia para a área de transferência" 66 | 67 | [Link-copied-to-clipboard] 68 | other = "Link copiado para a área de transferência!" 69 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/i18n/ru.toml: -------------------------------------------------------------------------------- 1 | [Search-placeholder] 2 | other = "Поиск..." 3 | 4 | [Clear-History] 5 | other = "Очистить историю" 6 | 7 | [Attachments-label] 8 | other = "Присоединенные файлы" 9 | 10 | [title-404] 11 | other = "Ошибка" 12 | 13 | [message-404] 14 | other = "Упс. Выглядит будто такой страницы нет ¯\\_(ツ)_/¯." 15 | 16 | [Go-to-homepage] 17 | other = "Перейти на главную" 18 | 19 | [Edit-this-page] 20 | other = "редактировать" 21 | 22 | [Print-this-chapter] 23 | other = "Печать всей главы" 24 | 25 | [Shortcuts-Title] 26 | other = "Еще" 27 | 28 | [Expand-title] 29 | other = "Развернуть..." 30 | 31 | [Navigation-toggle] 32 | other = "Меню" 33 | 34 | [Toc-toggle] 35 | other = "Оглавление" 36 | 37 | [Byte-symbol] 38 | other = "Б" 39 | 40 | [Kilobyte-symbol] 41 | other = "КБ" 42 | 43 | [Megabyte-symbol] 44 | other = "МБ" 45 | 46 | [note] 47 | other = "Заметка" 48 | 49 | [info] 50 | other = "Информация" 51 | 52 | [tip] 53 | other = "Совет" 54 | 55 | [warning] 56 | other = "Внимание" 57 | 58 | [Copy-to-clipboard] 59 | other = "Копировать в буфер" 60 | 61 | [Copied-to-clipboard] 62 | other = "Скопировано в буфер обмена!" 63 | 64 | [Copy-link-to-clipboard] 65 | other = "Скопировать ссылку в буфер обмена" 66 | 67 | [Link-copied-to-clipboard] 68 | other = "Ссылка скопирована в буфер обмена!" 69 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/i18n/tr.toml: -------------------------------------------------------------------------------- 1 | [Search-placeholder] 2 | other = "Ara..." 3 | 4 | [Clear-History] 5 | other = "Geçmişi Temizle" 6 | 7 | [Attachments-label] 8 | other = "Ekler" 9 | 10 | [title-404] 11 | other = "Hata" 12 | 13 | [message-404] 14 | other = "Uups. Görünüşe göre böyle bir sayfa yok ¯\\_(ツ)_/¯" 15 | 16 | [Go-to-homepage] 17 | other = "Anasayfaya dön" 18 | 19 | [Edit-this-page] 20 | other = "Düzenlemek" 21 | 22 | [Print-this-chapter] 23 | other = "Bölümün tamamını yazdır" 24 | 25 | [Shortcuts-Title] 26 | other = "Dahası Var" 27 | 28 | [Expand-title] 29 | other = "Genişlet..." 30 | 31 | [Navigation-toggle] 32 | other = "Menü" 33 | 34 | [Toc-toggle] 35 | other = "İçindekiler" 36 | 37 | [Byte-symbol] 38 | other = "B" 39 | 40 | [Kilobyte-symbol] 41 | other = "KB" 42 | 43 | [Megabyte-symbol] 44 | other = "MB" 45 | 46 | [note] 47 | other = "Not" 48 | 49 | [info] 50 | other = "Bilgi" 51 | 52 | [tip] 53 | other = "Bahşiş" 54 | 55 | [warning] 56 | other = "Uyarı" 57 | 58 | [Copy-to-clipboard] 59 | other = "Panoya kopyala" 60 | 61 | [Copied-to-clipboard] 62 | other = "Panoya kopyalanmış!" 63 | 64 | [Copy-link-to-clipboard] 65 | other = "Bağlantıyı panoya kopyala" 66 | 67 | [Link-copied-to-clipboard] 68 | other = "Panoya bağlantı kopyalanmış!" 69 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/i18n/vn.toml: -------------------------------------------------------------------------------- 1 | [Search-placeholder] 2 | other = "Tìm kiếm..." 3 | 4 | [Clear-History] 5 | other = "Xóa lịch sử.." 6 | 7 | [Attachments-label] 8 | other = "Tập tin đính kèm" 9 | 10 | [title-404] 11 | other = "Lỗi" 12 | 13 | [message-404] 14 | other = "Tiếc quá! Có vẻ như trang này không tồn tại ¯\\_(ツ)_/¯." 15 | 16 | [Go-to-homepage] 17 | other = "Đi đến trang chủ" 18 | 19 | [Edit-this-page] 20 | other = "Biên tập" 21 | 22 | [Print-this-chapter] 23 | other = "In toàn bộ chương" 24 | 25 | [Shortcuts-Title] 26 | other = "Nội dung khác" 27 | 28 | [Expand-title] 29 | other = "Mở rộng..." 30 | 31 | [Navigation-toggle] 32 | other = "Menu" 33 | 34 | [Toc-toggle] 35 | other = "Mục lục" 36 | 37 | [BinaryPrefix-kilobyte] 38 | other = "kb" 39 | 40 | [Byte-symbol] 41 | other = "B" 42 | 43 | [Kilobyte-symbol] 44 | other = "KB" 45 | 46 | [Megabyte-symbol] 47 | other = "MB" 48 | 49 | [note] 50 | other = "Ghi chú" 51 | 52 | [info] 53 | other = "Thông tin" 54 | 55 | [tip] 56 | other = "Mẹo vặt" 57 | 58 | [warning] 59 | other = "Cảnh báo" 60 | 61 | [Copy-to-clipboard] 62 | other = "Sao chép vào bảng tạm" 63 | 64 | [Copied-to-clipboard] 65 | other = "Sao chép vào bảng tạm!" 66 | 67 | [Copy-link-to-clipboard] 68 | other = "Sao chép nối kết vào bảng tạm" 69 | 70 | [Link-copied-to-clipboard] 71 | other = "Liên kết được sao chép vào bảng tạm!" 72 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/i18n/zh-cn.toml: -------------------------------------------------------------------------------- 1 | [Search-placeholder] 2 | other = "搜索..." 3 | 4 | [Clear-History] 5 | other = "清理历史记录" 6 | 7 | [Attachments-label] 8 | other = "附件" 9 | 10 | [title-404] 11 | other = "错误" 12 | 13 | [message-404] 14 | other = "哎哟。 看起来这个页面不存在 ¯\\_(ツ)_/¯。" 15 | 16 | [Go-to-homepage] 17 | other = "转到主页" 18 | 19 | [Edit-this-page] 20 | other = "编辑" 21 | 22 | [Print-this-chapter] 23 | other = "打印整章" 24 | 25 | [Shortcuts-Title] 26 | other = "更多" 27 | 28 | [Expand-title] 29 | other = "展开" 30 | 31 | [Navigation-toggle] 32 | other = "导航" 33 | 34 | [Toc-toggle] 35 | other = "目录" 36 | 37 | [Byte-symbol] 38 | other = "B" 39 | 40 | [Kilobyte-symbol] 41 | other = "KB" 42 | 43 | [Megabyte-symbol] 44 | other = "MB" 45 | 46 | [note] 47 | other = "注释" 48 | 49 | [info] 50 | other = "信息" 51 | 52 | [tip] 53 | other = "提示" 54 | 55 | [warning] 56 | other = "警告" 57 | 58 | [Copy-to-clipboard] 59 | other = "复制到剪贴板" 60 | 61 | [Copied-to-clipboard] 62 | other = "复制到剪贴板!" 63 | 64 | [Copy-link-to-clipboard] 65 | other = "将链接复制到剪贴板" 66 | 67 | [Link-copied-to-clipboard] 68 | other = "链接复制到剪贴板!" 69 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/i18n/zh-tw.toml: -------------------------------------------------------------------------------- 1 | [Search-placeholder] 2 | other = "搜尋..." 3 | 4 | [Clear-History] 5 | other = "清除歷史紀錄" 6 | 7 | [Attachments-label] 8 | other = "附件" 9 | 10 | [title-404] 11 | other = "錯誤" 12 | 13 | [message-404] 14 | other = "這個網頁已經被刪除、移動或從未存在" 15 | 16 | [Go-to-homepage] 17 | other = "回到首頁" 18 | 19 | [Edit-this-page] 20 | other = "編輯網頁" 21 | 22 | [Print-this-chapter] 23 | other = "列印整章" 24 | 25 | [Shortcuts-Title] 26 | other = "更多" 27 | 28 | [Expand-title] 29 | other = "展開" 30 | 31 | [Navigation-toggle] 32 | other = "導航" 33 | 34 | [Toc-toggle] 35 | other = "目錄" 36 | 37 | [Byte-symbol] 38 | other = "B" 39 | 40 | [Kilobyte-symbol] 41 | other = "KB" 42 | 43 | [Megabyte-symbol] 44 | other = "MB" 45 | 46 | [note] 47 | other = "註釋" 48 | 49 | [info] 50 | other = "資訊" 51 | 52 | [tip] 53 | other = "提示" 54 | 55 | [warning] 56 | other = "警告" 57 | 58 | [Copy-to-clipboard] 59 | other = "複製到剪貼板" 60 | 61 | [Copied-to-clipboard] 62 | other = "複製到剪貼簿!" 63 | 64 | [Copy-link-to-clipboard] 65 | other = "將連結複製到剪貼簿" 66 | 67 | [Link-copied-to-clipboard] 68 | other = "連結複製到剪貼簿!" 69 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choria-io/asyncjobs/f1ba2250e3990bcac45d6cf6ba94f74366ad8be4/docs/themes/hugo-theme-relearn/images/screenshot.png -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/images/tn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choria-io/asyncjobs/f1ba2250e3990bcac45d6cf6ba94f74366ad8be4/docs/themes/hugo-theme-relearn/images/tn.png -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/404.html: -------------------------------------------------------------------------------- 1 | {{- .Scratch.Set "relearnOutputFormat" "HTML" }} 2 | 3 | 4 | 5 | {{- partial "meta.html" . }} 6 | {{- .Scratch.Add "title" "" }} 7 | {{- if eq .Site.Data.titles .Title }} 8 | {{- .Scratch.Set "title" (index .Site.Data.titles .Title).title }} 9 | {{- else }} 10 | {{- .Scratch.Set "title" .Title}} 11 | {{- end }} 12 | {{ .Scratch.Get "title" }} {{ default "::" .Site.Params.titleSeparator }} {{ .Site.Title }} 13 | 14 | {{- partial "favicon.html" . }} 15 | {{- partial "stylesheet.html" . }} 16 | 24 | {{- partial "custom-header.html" . }} 25 | 26 | 27 |
28 | 29 |
30 |
31 |

{{ T "title-404" }}

32 |

33 |

{{ T "message-404" }}

34 |

35 |

{{ T "Go-to-homepage" }}

36 |

Page not found!

37 |
38 |
39 |
40 | 41 | 42 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/_default/_markup/render-codeblock-mermaid.html: -------------------------------------------------------------------------------- 1 |
2 | {{- safeHTML .Inner -}} 3 |
4 | {{- .Page.Store.Set "htmlHasMermaid" true }} 5 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/_default/list.html: -------------------------------------------------------------------------------- 1 | {{- .Scratch.Set "relearnOutputFormat" "HTML" }} 2 | {{- partial "header.html" . }} 3 | {{- partial "content-screen.html" . }} 4 | {{- partial "footer.html" . }} -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/_default/list.print.html: -------------------------------------------------------------------------------- 1 | {{- .Scratch.Set "relearnOutputFormat" "PRINT" }} 2 | {{- partial "header.html" . }} 3 | {{- partial "content-print.html" . }} 4 | {{- partial "footer.html" . }} -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/_default/rss.xml: -------------------------------------------------------------------------------- 1 | {{- printf "" | safeHTML }} 2 | {{- partial "page-meta.got" . }} 3 | {{- $pages := .Page.Pages }} 4 | {{- if .Page.IsHome }} 5 | {{- $pages = .Page.Sections }} 6 | {{- else if .Page.Sections}} 7 | {{- $pages = (.Page.Pages | union .Page.Sections) }} 8 | {{- end }} 9 | {{- $limit := .Site.Config.Services.RSS.Limit -}} 10 | {{- if ge $limit 0 -}} 11 | {{- $pages = $pages | first $limit -}} 12 | {{- end }} 13 | 14 | 15 | {{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} on {{ end }}{{ .Site.Title }}{{ end }} 16 | {{ .Permalink }} 17 | Recent content {{ if ne .Title .Site.Title }}{{ with .Title }}in {{.}} {{ end }}{{ end }}on {{ .Site.Title }} 18 | Hugo -- gohugo.io{{ with .Site.LanguageCode }} 19 | {{.}}{{end}}{{ with .Site.Author.email }} 20 | {{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}{{end}}{{ with .Site.Author.email }} 21 | {{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}{{end}}{{ with .Site.Copyright }} 22 | {{.}}{{end}}{{ if not .Date.IsZero }} 23 | {{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}{{ end }} 24 | {{- with .OutputFormats.Get "RSS" -}} 25 | {{ printf "" .Permalink .MediaType | safeHTML }} 26 | {{- end -}} 27 | {{- range $pages }} 28 | {{- if and .Permalink .Title (or (ne (.Scratch.Get "relearnIsHiddenStem") true) (ne .Site.Params.disableSeoHiddenPages true) ) }} 29 | 30 | {{ .Title }} 31 | {{ .Permalink }} 32 | {{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }} 33 | {{- with .Site.Author.email }} 34 | {{.}}{{ with $.Site.Author.name }} ({{.}}){{end}} 35 | {{- end }} 36 | {{ .Permalink }} 37 | {{ .Summary | html }} 38 | 39 | {{- end }} 40 | {{- end }} 41 | 42 | 43 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/_default/single.html: -------------------------------------------------------------------------------- 1 | {{- .Scratch.Set "relearnOutputFormat" "HTML" }} 2 | {{- partial "header.html" . }} 3 | {{- partial "content-screen.html" . }} 4 | {{- partial "footer.html" . }} -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/_default/single.print.html: -------------------------------------------------------------------------------- 1 | {{- .Scratch.Set "relearnOutputFormat" "PRINT" }} 2 | {{- partial "header.html" . }} 3 | {{- partial "content-print.html" . }} 4 | {{- partial "footer.html" . }} -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/_default/sitemap.xml: -------------------------------------------------------------------------------- 1 | {{ printf "" | safeHTML }} 2 | {{- partial "page-meta.got" . }} 3 | 4 | {{- range .Data.Pages }} 5 | {{- if and .Title (or (ne (.Scratch.Get "relearnIsHiddenStem") true) (ne .Site.Params.disableSeoHiddenPages true) ) }} 6 | 7 | {{ .Permalink }}{{ if not .Lastmod.IsZero }} 8 | {{ safeHTML ( .Lastmod.Format "2006-01-02T15:04:05-07:00" ) }}{{ end }}{{ with .Sitemap.ChangeFreq }} 9 | {{ . }}{{ end }}{{ if ge .Sitemap.Priority 0.0 }} 10 | {{ .Sitemap.Priority }}{{ end }}{{ if .IsTranslated }}{{ range .Translations }} 11 | {{ end }} 16 | {{ end }} 21 | 22 | {{- end -}} 23 | {{- end }} 24 | 25 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/_default/taxonomy.html: -------------------------------------------------------------------------------- 1 | {{- .Scratch.Set "relearnOutputFormat" "HTML" }} 2 | {{- partial "header.html" . }} 3 |
4 | 5 |

{{ if eq .Kind "term" }}{{ .Data.Singular | humanize }} {{ default "::" .Site.Params.titleSeparator }} {{ end }}{{ .Title }}

6 |
    7 | {{- range .Data.Terms.Alphabetical }} 8 | {{- if and .Page.Title (or (ne (.Page.Scratch.Get "relearnIsHiddenStem") true) (ne .Page.Site.Params.disableTagHiddenPages true) ) }} 9 |
  • {{ .Page.Title }} ({{ len .Pages }})
  • 10 | {{- end }} 11 | {{- else }} 12 | {{- range sort .Pages "Title" }} 13 | {{- if and .Title (or (ne (.Scratch.Get "relearnIsHiddenStem") true) (ne .Site.Params.disableTagHiddenPages true) ) }} 14 |
  • {{ .Title }}
  • 15 | {{- end }} 16 | {{- end }} 17 | {{- end }} 18 |
19 | 20 |
21 |
22 |
23 | {{- partial "footer.html" . }} -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/_default/taxonomy.print.html: -------------------------------------------------------------------------------- 1 | {{- .Scratch.Set "relearnOutputFormat" "PRINT" }} 2 | {{- partial "header.html" . }} 3 |
4 | 5 |

{{ if eq .Kind "term" }}{{ .Data.Singular | humanize }} {{ default "::" .Site.Params.titleSeparator }} {{ end }}{{ .Title }}

6 |
    7 | {{- range .Data.Terms.Alphabetical }} 8 | {{- if and .Page.Title (or (ne (.Page.Scratch.Get "relearnIsHiddenStem") true) (ne .Page.Site.Params.disableTagHiddenPages true) ) }} 9 |
  • {{ .Page.Title }} ({{ len .Pages }})
  • 10 | {{- end }} 11 | {{- else }} 12 | {{- range sort .Pages "Title" }} 13 | {{- if and .Title (or (ne (.Scratch.Get "relearnIsHiddenStem") true) (ne .Site.Params.disableTagHiddenPages true) ) }} 14 |
  • {{ .Title }}
  • 15 | {{- end }} 16 | {{- end }} 17 | {{- end }} 18 |
19 | 20 |
21 |
22 |
23 | {{- partial "footer.html" . }} -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/index.html: -------------------------------------------------------------------------------- 1 | {{- .Scratch.Set "relearnOutputFormat" "HTML" }} 2 | {{- partial "header.html" . }} 3 | {{- if .Site.Home.Content }} 4 | {{- partial "content-screen.html" .Site.Home }} 5 | {{- else }} 6 |
7 | 8 |

Customize your own home page

9 |

10 | The site is working. Don't forget to customize this page with your own. You typically have 3 choices : 11 |

12 |
    13 |
  • 1. Create an _index.md document in content folder and fill it with Markdown content
  • 14 |
  • 2. Create an index.html file in the static folder and fill the file with HTML content
  • 15 |
  • 3. Configure your server to automatically redirect home page to one your documentation page
  • 16 |
17 | 18 |
19 |
20 |
21 | {{- end }} 22 | {{- partial "footer.html" . }} -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/index.json: -------------------------------------------------------------------------------- 1 | {{- $pages := slice }} 2 | {{- range .Site.Pages }} 3 | {{- if and .Title (or (ne (.Scratch.Get "relearnIsHiddenStem") true) (ne .Site.Params.disableSearchHiddenPages true) ) }} 4 | {{- $pages = $pages | append (dict "uri" .RelPermalink "title" .Title "tags" .Params.tags "description" .Description "content" (.Plain | htmlUnescape)) }} 5 | {{- end }} 6 | {{- end }} 7 | {{- $pages | jsonify (dict "indent" " ") }} 8 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/index.print.html: -------------------------------------------------------------------------------- 1 | {{- .Scratch.Set "relearnOutputFormat" "PRINT" }} 2 | {{- partial "header.html" . }} 3 | {{- if .Site.Home.Content }} 4 | {{- partial "content-print.html" .Site.Home }} 5 | {{- else }} 6 |
7 | 8 |

Customize your own home page

9 |

10 | The site is working. Don't forget to customize this page with your own. You typically have 3 choices : 11 |

12 |
    13 |
  • 1. Create an _index.md document in content folder and fill it with Markdown content
  • 14 |
  • 2. Create an index.html file in the static folder and fill the file with HTML content
  • 15 |
  • 3. Configure your server to automatically redirect home page to one your documentation page
  • 16 |
17 | 18 |
19 |
20 |
21 | {{- end }} 22 | {{- partial "footer.html" . }} -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/partials/article.html: -------------------------------------------------------------------------------- 1 | {{- $content := .content }} 2 | {{- with .page }} 3 | 4 | 5 | {{ if and (not .IsHome ) (not .Params.chapter) }}

{{ .Title }}

6 | {{ end }}{{ $content | safeHTML }} 7 |
8 | {{- partial "content-footer.html" . }} 9 |
10 | 11 | {{- end }} -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/partials/content-footer.html: -------------------------------------------------------------------------------- 1 | {{- with .Params.LastModifierDisplayName }} 2 | {{ with $.Params.LastModifierEmail }}{{ end }}{{ . }}{{ with $.Params.LastModifierEmail }}{{ end }} 3 | {{- with $.Date }} 4 | {{ . | time.Format ":date_medium" }} 5 | {{- end }} 6 | {{- end }} -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/partials/content-print.html: -------------------------------------------------------------------------------- 1 | {{- $currentNode := . }} 2 | {{- $isActive := .IsHome }} 3 | {{- $pages := .Site.Home.Sections }} 4 | {{- $defaultOrdersectionsby := .Site.Params.ordersectionsby | default "weight" }} 5 | {{- $currentOrdersectionsby := .Site.Home.Params.ordersectionsby | default $defaultOrdersectionsby }} 6 | {{- if $isActive }} 7 | {{- template "section-print" dict "sect" . "currentnode" $currentNode }} 8 | {{- if or .IsHome .Params.chapter $pages }} 9 |
10 | {{- end }} 11 | {{- end }} 12 | {{- if eq $currentOrdersectionsby "title" }} 13 | {{- range $pages.ByTitle }} 14 | {{- template "section-tree-print" dict "sect" . "currentnode" $currentNode "isActive" $isActive }} 15 | {{- end }} 16 | {{- else }} 17 | {{- range $pages.ByWeight }} 18 | {{- template "section-tree-print" dict "sect" . "currentnode" $currentNode "isActive" $isActive }} 19 | {{- end }} 20 | {{- end }} 21 | {{- if $isActive }} 22 | {{- if or .IsHome .Params.chapter $pages }} 23 |
24 | {{- end }} 25 | {{- end }} 26 | {{- define "section-tree-print" }} 27 | {{- $currentNode := .currentnode }} 28 | {{- $isActive := .isActive }} 29 | {{- $currentFileRelPermalink := .currentnode.RelPermalink }} 30 | {{- with .sect }} 31 | {{- $currentIsActive := eq .RelPermalink $currentFileRelPermalink }} 32 | {{- $isActive = or $currentIsActive $isActive }} 33 | {{- $pages := .Pages }} 34 | {{- if .Page.IsHome }} 35 | {{- $pages = .Sections }} 36 | {{- else if .Page.Sections}} 37 | {{- $pages = (.Pages | union .Sections) }} 38 | {{- end }} 39 | {{- $relearnIsHiddenFrom := index ($currentNode.Scratch.Get "relearnIsHiddenFrom") .RelPermalink }} 40 | {{- $hidden := and $relearnIsHiddenFrom (not $.showhidden) (not (.IsAncestor $currentNode)) }} 41 | {{- if $hidden }} 42 | {{- else if or .IsSection .IsHome }} 43 | {{- $defaultOrdersectionsby := .Site.Params.ordersectionsby | default "weight" }} 44 | {{- $currentOrdersectionsby := .Params.ordersectionsby | default $defaultOrdersectionsby }} 45 | {{- if $isActive }} 46 | {{- template "section-print" dict "sect" . "currentnode" $currentNode }} 47 | {{- if or .IsHome .Params.chapter $pages }} 48 |
49 | {{- end }} 50 | {{- end }} 51 | {{- if eq $currentOrdersectionsby "title" }} 52 | {{- range $pages.ByTitle }} 53 | {{- template "section-tree-print" dict "sect" . "currentnode" $currentNode "isActive" $isActive }} 54 | {{- end }} 55 | {{- else }} 56 | {{- range $pages.ByWeight }} 57 | {{- template "section-tree-print" dict "sect" . "currentnode" $currentNode "isActive" $isActive }} 58 | {{- end }} 59 | {{- end }} 60 | {{- if $isActive }} 61 | {{- if or .IsHome .Params.chapter $pages }} 62 |
63 | {{- end }} 64 | {{- end }} 65 | {{- else }} 66 | {{- if $isActive }} 67 | {{- template "section-print" dict "sect" . "currentnode" $currentNode }} 68 | {{- end }} 69 | {{- end }} 70 | {{- end }} 71 | {{- end }} 72 | {{- define "section-print" }} 73 | {{- $currentNode := .currentnode }} 74 | {{- with .sect }} 75 | {{- $currentNode.Page.Store.Set "printHasMermaid" (or ($currentNode.Page.Store.Get "printHasMermaid") (.Page.Store.Get "htmlHasMermaid")) }} 76 | {{- $currentNode.Page.Store.Set "printHasSwagger" (or ($currentNode.Page.Store.Get "printHasSwagger") (.Page.Store.Get "htmlHasSwagger")) }} 77 | {{/* if we have a relative link in a print page, our print URL is one level to deep; so we are making it absolute to our page by prepending the page's permalink */}} 78 | {{- $link_prefix := strings.TrimRight "/" .Page.RelPermalink }} 79 | {{- $content := partial "content.html" . }} 80 | {{- $content = replaceRE "((?:src|href)\\s*=(?:\\s*[\"']\\s*)?)(\\.[^\"'\\s>]*|[\\w]+[^\"'\\s>:]*)([\"'\\s>])" (printf "${1}%s/${2}${3}" $link_prefix) $content }} 81 | {{- partial "article.html" (dict "page" . "content" $content) }} 82 | {{- end }} 83 | {{- end }} -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/partials/content-screen.html: -------------------------------------------------------------------------------- 1 | {{- partial "article.html" (dict "page" . "content" .Content) }} -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/partials/content.html: -------------------------------------------------------------------------------- 1 | {{- .Content }} -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/partials/custom-comments.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/partials/custom-footer.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/partials/custom-header.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/partials/favicon.html: -------------------------------------------------------------------------------- 1 | {{- $assetBusting := not .Site.Params.disableAssetsBusting }} 2 | {{- if (fileExists "/static/images/favicon.svg") }} 3 | 4 | {{- else if (fileExists "/static/images/favicon.png") }} 5 | 6 | {{- else if (fileExists "/static/images/favicon.ico") }} 7 | 8 | {{- else if (fileExists "/static/images/logo.svg") }} 9 | 10 | {{- else if (fileExists "/static/images/logo.png") }} 11 | 12 | {{- else if (fileExists "/static/images/logo.ico") }} 13 | 14 | {{- end }} -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/partials/footer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{- partial "custom-comments.html" . }} 5 | 6 | {{- partial "menu.html" . }} 7 | 8 | 9 | 10 | {{- $wantsMermaid := or (and (eq (.Scratch.Get "relearnOutputFormat") "HTML") (.Page.Store.Get "htmlHasMermaid")) (and (eq (.Scratch.Get "relearnOutputFormat") "PRINT") (.Page.Store.Get "printHasMermaid")) }} 11 | {{- if (or $wantsMermaid (and (ne .Params.disableMermaid nil) (not .Params.disableMermaid)) (and (ne .Site.Params.disableMermaid nil) (not .Site.Params.disableMermaid)) ) }} 12 | 13 | {{- if isset .Params "custommermaidurl" }} 14 | 15 | {{- else if isset .Site.Params "custommermaidurl" }} 16 | 17 | {{- else }} 18 | 19 | {{- end }} 20 | {{- if isset .Params "mermaidinitialize" }} 21 | {{- $.Scratch.Set "mermaidInitialize" .Params.mermaidInitialize }} 22 | {{- else if isset .Site.Params "mermaidinitialize" }} 23 | {{- $.Scratch.Set "mermaidInitialize" .Site.Params.mermaidInitialize }} 24 | {{- else }} 25 | {{- $.Scratch.Set "mermaidInitialize" "{}" }} 26 | {{- end }} 27 | 39 | {{- end }} 40 | {{- $wantsSwagger := or (and (eq (.Scratch.Get "relearnOutputFormat") "HTML") (.Page.Store.Get "htmlHasSwagger")) (and (eq (.Scratch.Get "relearnOutputFormat") "PRINT") (.Page.Store.Get "printHasSwagger")) }} 41 | {{- if (or $wantsSwagger (and (ne .Params.disableSwagger nil) (not .Params.disableSwagger)) (and (ne .Site.Params.disableSwagger nil) (not .Site.Params.disableSwagger)) ) }} 42 | {{- if isset .Params "customswaggerurl" }} 43 | 44 | {{- else if isset .Site.Params "customswaggerurl" }} 45 | 46 | {{- else }} 47 | 48 | {{- end }} 49 | {{- if isset .Params "swaggerinitialize" }} 50 | {{- $.Scratch.Set "swaggerInitialize" .Params.swaggerInitialize }} 51 | {{- else if isset .Site.Params "swaggerinitialize" }} 52 | {{- $.Scratch.Set "swaggerInitialize" .Site.Params.swaggerInitialize }} 53 | {{- else }} 54 | {{- $.Scratch.Set "swaggerInitialize" "{}" }} 55 | {{- end }} 56 | 65 | {{- end }} 66 | 67 | {{- partial "custom-footer.html" . }} 68 | 69 | 70 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/partials/menu-footer.html: -------------------------------------------------------------------------------- 1 | 2 |

Built with by Hugo

-------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/partials/menu-post.html: -------------------------------------------------------------------------------- 1 | {{ .Params.Post | safeHTML }} -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/partials/menu-pre.html: -------------------------------------------------------------------------------- 1 | {{ .Params.Pre | safeHTML }} -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/partials/meta.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{- $c:=""}}{{/* to avoid that user swiping to the left leaves a gap on the right side, we set minimum-scale, even if not advised to */}} 4 | 5 | {{ hugo.Generator }} 6 | {{- $ver := partial "version.html" }} 7 | {{- $ver = replaceRE "\\s*(\\S*)" "${1}" $ver }} 8 | 9 | {{- partial "page-meta.got" . }} 10 | {{- if not (and .Title (or (ne (.Scratch.Get "relearnIsHiddenStem") true) (ne .Site.Params.disableSeoHiddenPages true) ) ) }} 11 | 12 | {{- end }} 13 | 14 | {{- with .Site.Params.author }} 15 | 16 | {{- end }} 17 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/partials/page-meta.got: -------------------------------------------------------------------------------- 1 | {{- $currentNode := . }} 2 | {{- $currentNode.Scratch.Delete "relearnIsSelfFound" }} 3 | {{- $currentNode.Scratch.Delete "relearnPrevPage" }} 4 | {{- $currentNode.Scratch.Delete "relearnNextPage" }} 5 | {{- $currentNode.Scratch.Delete "relearnSubPages" }} 6 | {{- $currentNode.Scratch.Delete "relearnIsHiddenNode" }}{{/* the node itself is flagged as hidden */}} 7 | {{- $currentNode.Scratch.Delete "relearnIsHiddenStem" }}{{/* the node or one of its parents is flagged as hidden */}} 8 | {{- $currentNode.Scratch.Delete "relearnIsHiddenFrom" }}{{/* the node is hidden from the current page */}} 9 | {{- template "relearn-structure" dict "node" .Site.Home "currentnode" $currentNode "hiddenstem" false "hiddencurrent" false "defaultOrdersectionsby" .Site.Params.ordersectionsby }} 10 | {{- define "relearn-structure" }} 11 | {{- $currentNode := .currentnode }} 12 | {{- $isSelf := eq $currentNode.RelPermalink .node.RelPermalink }} 13 | {{- $isDescendant := and (not $isSelf) (.node.IsDescendant $currentNode) }} 14 | {{- $isAncestor := and (not $isSelf) (.node.IsAncestor $currentNode) }} 15 | {{- $isOther := and (not $isDescendant) (not $isSelf) (not $isAncestor) }} 16 | 17 | {{- if $isSelf }} 18 | {{- $currentNode.Scratch.Set "relearnIsSelfFound" true }} 19 | {{- end}} 20 | {{- $isSelfFound := eq ($currentNode.Scratch.Get "relearnIsSelfFound") true }} 21 | {{- $isPreSelf := and (not $isSelfFound) (not $isSelf) }} 22 | {{- $isPostSelf := and ($isSelfFound) (not $isSelf) }} 23 | 24 | {{- $hidden_node := or (.node.Params.hidden) (eq .node.Title "") }} 25 | {{- $hidden_stem:= or $hidden_node .hiddenstem }} 26 | {{- $hidden_current_stem:= or $hidden_node .hiddencurrent }} 27 | {{- $hidden_from_current := or (and $hidden_node (not $isAncestor) (not $isSelf) ) (and .hiddencurrent (or $isPreSelf $isPostSelf $isDescendant) ) }} 28 | {{- $currentNode.Scratch.SetInMap "relearnIsHiddenNode" .node.RelPermalink $hidden_node }} 29 | {{- $currentNode.Scratch.SetInMap "relearnIsHiddenStem" .node.RelPermalink $hidden_stem }} 30 | {{- $currentNode.Scratch.SetInMap "relearnIsHiddenFrom" .node.RelPermalink $hidden_current_stem }} 31 | 32 | {{- if not $hidden_from_current }} 33 | {{- if $isPreSelf }} 34 | {{- $currentNode.Scratch.Set "relearnPrevPage" .node }} 35 | {{- else if and $isPostSelf (eq ($currentNode.Scratch.Get "relearnNextPage") nil) }} 36 | {{- $currentNode.Scratch.Set "relearnNextPage" .node }} 37 | {{- end}} 38 | {{- end }} 39 | 40 | {{- $currentNode.Scratch.Set "relearnSubPages" .node.Pages }} 41 | {{- if .node.IsHome }} 42 | {{- $currentNode.Scratch.Set "relearnSubPages" .node.Sections }} 43 | {{- else if .node.Sections }} 44 | {{- $currentNode.Scratch.Set "relearnSubPages" (.node.Pages | union .node.Sections) }} 45 | {{- end }} 46 | {{- $pages := ($currentNode.Scratch.Get "relearnSubPages") }} 47 | 48 | {{- $defaultOrdersectionsby := .defaultOrdersectionsby }} 49 | {{- $currentOrdersectionsby := .node.Params.ordersectionsby | default $defaultOrdersectionsby }} 50 | 51 | {{- if eq $currentOrdersectionsby "title"}} 52 | {{- range $pages.ByTitle }} 53 | {{- template "relearn-structure" dict "node" . "currentnode" $currentNode "hiddenstem" $hidden_stem "hiddencurrent" $hidden_from_current "defaultOrdersectionsby" $defaultOrdersectionsby }} 54 | {{- end}} 55 | {{- else}} 56 | {{- range $pages.ByWeight }} 57 | {{- template "relearn-structure" dict "node" . "currentnode" $currentNode "hiddenstem" $hidden_stem "hiddencurrent" $hidden_from_current "defaultOrdersectionsby" $defaultOrdersectionsby }} 58 | {{- end}} 59 | {{- end }} 60 | {{- end }} -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/partials/search.html: -------------------------------------------------------------------------------- 1 | 6 | {{- $assetBusting := not .Site.Params.disableAssetsBusting }} 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/partials/stylesheet.html: -------------------------------------------------------------------------------- 1 | {{- $assetBusting := not .Site.Params.disableAssetsBusting }} 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{- $themevariants := slice | append (.Site.Params.themeVariant | default "relearn-light" ) }} 9 | {{- with index $themevariants 0 }} 10 | 11 | {{- end }} 12 | 13 | 14 | {{- range .Site.Params.custom_css }} 15 | 16 | {{- end }} 17 | {{- if .Site.Params.disableInlineCopyToClipBoard }} 18 | 28 | {{- end }} 29 | 30 | 49 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/partials/tags.html: -------------------------------------------------------------------------------- 1 | 2 | {{- if .Params.tags }} 3 |
4 | {{- range sort .Params.tags }} 5 | {{ . }} 6 | {{- end }} 7 |
8 | {{- end }} -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/partials/toc.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | {{ .TableOfContents }} 5 |
6 |
-------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/partials/version.html: -------------------------------------------------------------------------------- 1 | 3.4.1 2 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/shortcodes/attachments.html: -------------------------------------------------------------------------------- 1 | {{- $_hugo_config := `{ "version": 1 }` }} 2 | {{- $style := .Get "style" | default "transparent" }} 3 | {{- $title := .Get "title" | default ("Attachments-label" | T) }} 4 | {{- $sort := .Get "sort" | default "asc" }} 5 | {{- $icon := .Get "icon" | default "" }} 6 | {{- if and (not $icon) (eq (len $icon) 0) }} 7 | {{- $icon = "paperclip" }} 8 | {{- end }} 9 | {{- $icon = trim $icon " " }} 10 |
11 |
{{ if $icon }} {{ end }}{{ $title }}
12 |
    13 | {{- $filesName := "files" }} 14 | {{- if ne .Page.File.BaseFileName "index" }} 15 | {{- $filesName = printf "%s.files" .Page.File.BaseFileName }} 16 | {{- end}} 17 | 18 | {{- $fileLink := printf "%s/%s" (.Page.Language.ContentDir | default "content") .Page.File.Dir }} 19 | {{- $fileLink = replace (replace $fileLink "\\" "/") "content/" "" }} 20 | {{- $fileDir := printf "%s/%s" (.Page.Language.ContentDir | default "content") .Page.File.Dir }} 21 | {{- $fileDir = replace $fileDir "\\" "/" }} 22 | {{- $pattern := .Get "pattern" | default "" }} 23 | {{- range sort (readDir (printf "%s%s" $fileDir $filesName) ) "Name" $sort }} 24 | {{- if findRE $pattern .Name}} 25 | {{- $size := .Size }} 26 | {{- $unit := "Byte-symbol" }} 27 | {{- if ge $size 1024 }} 28 | {{- $size = div $size 1024 }} 29 | {{- $unit = "Kilobyte-symbol" }} 30 | {{- end }} 31 | {{- if ge $size 1024 }} 32 | {{- $size = div $size 1024 }} 33 | {{- $unit = "Megabyte-symbol" }} 34 | {{- end }} 35 | {{- $unitsymbol := $unit | T }} 36 |
  • {{.Name}} ({{$size}} {{$unitsymbol}})
  • 37 | {{- end }} 38 | {{- end }} 39 |
40 | {{- .Inner }} 41 |
42 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/shortcodes/button.html: -------------------------------------------------------------------------------- 1 | {{- $_hugo_config := `{ "version": 1 }` }} 2 | 3 | {{- $icon := .Get "icon" }} 4 | {{- $iconposition := .Get "icon-position" }} 5 | {{- if ($icon) }} 6 | {{- if or (not ($iconposition)) (eq $iconposition "left") }} 7 | 8 | {{- end }} 9 | {{- end }} 10 | {{ .Inner }} 11 | {{- if and ($icon) (eq $iconposition "right")}} 12 | 13 | {{- end }} 14 | 15 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/shortcodes/expand.html: -------------------------------------------------------------------------------- 1 | {{- $_hugo_config := `{ "version": 1 }` }} 2 | {{- $title := .Get 0 | default (T "Expand-title") }} 3 | {{- $content := .Inner | safeHTML }} 4 | {{- $expanded := eq (.Get 1) "true" }} 5 |
8 | {{/* things are getting complicated when search tries to open the expand box while jquery sets the display CSS on the element */}}{{ "" -}} 9 | 10 | 11 | 12 | {{ $title }} 13 | 14 |
17 | {{ $content }} 18 |
19 |
-------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/shortcodes/include.html: -------------------------------------------------------------------------------- 1 | {{- $file := .Get 0 }} 2 | {{- $showFirstHeading := .Get 1 | default true }} 3 | {{- if not $showFirstHeading }}
{{ end }} 4 | 5 | {{ $file | readFile | safeHTML }} 6 | 7 | {{- if not $showFirstHeading }}
{{ end }} -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/shortcodes/mermaid.html: -------------------------------------------------------------------------------- 1 | {{- $_hugo_config := `{ "version": 1 }` }} 2 |
3 | {{- safeHTML .Inner -}} 4 |
5 | {{- .Page.Store.Set "htmlHasMermaid" true }} -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/shortcodes/notice.html: -------------------------------------------------------------------------------- 1 | {{- $_hugo_config := `{ "version": 1 }` }} 2 | {{- $style := .Get 0 }} 3 | {{- $title := .Get 1 | default ($style | T) }} 4 | {{- $icon := .Get 2 }} 5 | {{- if and (not $icon) (eq (len $icon) 0) }} 6 | {{- if eq $style "info" }}{{ $icon = default "info-circle" }}{{ end }} 7 | {{- if eq $style "warning" }}{{ $icon = default "exclamation-triangle" }}{{ end }} 8 | {{- if eq $style "note" }}{{ $icon = default "exclamation-circle" }}{{ end }} 9 | {{- if eq $style "tip" }}{{ $icon = default "lightbulb " }}{{ end }} 10 | {{- end }} 11 | {{- $icon = trim $icon " " }} 12 |
13 |
{{ if $icon }} {{ end }}{{ $title }}
14 |
15 | {{ .Inner }} 16 |
17 |
-------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/shortcodes/siteparam.html: -------------------------------------------------------------------------------- 1 | {{- $paramName := (.Get 0) -}} 2 | {{- $siteParams := .Site.Params -}} 3 | {{- with $paramName -}} 4 | {{- with $siteParams -}} 5 | {{- index . (lower $paramName) -}} 6 | {{- end -}} 7 | {{- end -}} -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/shortcodes/swagger.html: -------------------------------------------------------------------------------- 1 | {{- $original := .Get "src" }} 2 | {{- with .Page.Resources.Match $original }} 3 | {{- range . }} 4 | {{- $original = .RelPermalink }} 5 | {{- end }} 6 | {{- end }} 7 | 20 | {{- .Page.Store.Set "htmlHasSwagger" true }} -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/shortcodes/tab.html: -------------------------------------------------------------------------------- 1 | {{ if .Parent }} 2 | {{ $name := trim (.Get "name") " " }} 3 | {{ if not (.Parent.Scratch.Get "tabs") }} 4 | {{ .Parent.Scratch.Set "tabs" slice }} 5 | {{ end }} 6 | {{ with .Inner }} 7 | {{ $.Parent.Scratch.Add "tabs" (dict "name" $name "content" . ) }} 8 | {{ end }} 9 | {{ else }} 10 | {{- errorf "[%s] %q: tab shortcode missing its parent" site.Language.Lang .Page.Path -}} 11 | {{ end}} 12 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/layouts/shortcodes/tabs.html: -------------------------------------------------------------------------------- 1 | {{- with .Inner }}{{/* don't do anything, just call it */}}{{ end }} 2 | {{- $groupId := default "default" (.Get "groupId") }} 3 |
4 |
5 | {{- range $idx, $tab := .Scratch.Get "tabs" }} 6 | 12 | {{- end }} 13 |
14 |
15 | {{- range $idx, $tab := .Scratch.Get "tabs" }} 16 |
17 | {{ .content }} 18 |
19 | {{- end }} 20 |
21 |
22 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/css/auto-complete.css: -------------------------------------------------------------------------------- 1 | .autocomplete-suggestions { 2 | text-align: left; 3 | cursor: default; 4 | border: 1px solid #ccc; 5 | border-top: 0; 6 | background: #fff; 7 | box-shadow: -1px 1px 3px rgba(0,0,0,.1); 8 | 9 | /* core styles should not be changed */ 10 | position: absolute; 11 | display: none; 12 | z-index: 9999; 13 | max-height: 254px; 14 | overflow: hidden; 15 | overflow-y: auto; 16 | box-sizing: border-box; 17 | } 18 | .autocomplete-suggestion { 19 | position: relative; 20 | cursor: pointer; 21 | padding: 7px; 22 | line-height: 23px; 23 | white-space: nowrap; 24 | overflow: hidden; 25 | text-overflow: ellipsis; 26 | color: #333; 27 | } 28 | 29 | .autocomplete-suggestion b { 30 | font-weight: normal; 31 | color: #1f8dd6; 32 | } 33 | 34 | .autocomplete-suggestion.selected { 35 | background: #333; 36 | color: #fff; 37 | } 38 | 39 | .autocomplete-suggestion:hover { 40 | background: #444; 41 | color: #fff; 42 | } 43 | 44 | .autocomplete-suggestion > .context { 45 | font-size: 12px; 46 | overflow: hidden; 47 | text-overflow: ellipsis; 48 | } 49 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/css/chroma-learn.css: -------------------------------------------------------------------------------- 1 | /* based on base16-snazzy 2 | /* Background */ .chroma { color: #e2e4e5; background-color: #282a36 } 3 | /* Other */ .chroma .x { } 4 | /* Error */ .chroma .err { color: #ff5c57 } 5 | /* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; } 6 | /* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; width: auto; overflow: auto; display: block; } 7 | /* LineHighlight */ .chroma .hl { display: block; width: 100%;background-color: #ffffcc } 8 | /* LineNumbersTable */ .chroma .lnt { margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f } 9 | /* LineNumbers */ .chroma .ln { margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f } 10 | /* Keyword */ .chroma .k { color: #ff6ac1 } 11 | /* KeywordConstant */ .chroma .kc { color: #ff6ac1 } 12 | /* KeywordDeclaration */ .chroma .kd { color: #ff5c57 } 13 | /* KeywordNamespace */ .chroma .kn { color: #ff6ac1 } 14 | /* KeywordPseudo */ .chroma .kp { color: #ff6ac1 } 15 | /* KeywordReserved */ .chroma .kr { color: #ff6ac1 } 16 | /* KeywordType */ .chroma .kt { color: #9aedfe } 17 | /* Name */ .chroma .n { } 18 | /* NameAttribute */ .chroma .na { color: #57c7ff } 19 | /* NameBuiltin */ .chroma .nb { color: #ff5c57 } 20 | /* NameBuiltinPseudo */ .chroma .bp { } 21 | /* NameClass */ .chroma .nc { color: #f3f99d } 22 | /* NameConstant */ .chroma .no { color: #ff9f43 } 23 | /* NameDecorator */ .chroma .nd { color: #ff9f43 } 24 | /* NameEntity */ .chroma .ni { } 25 | /* NameException */ .chroma .ne { } 26 | /* NameFunction */ .chroma .nf { color: #57c7ff } 27 | /* NameFunctionMagic */ .chroma .fm { } 28 | /* NameLabel */ .chroma .nl { color: #ff5c57 } 29 | /* NameNamespace */ .chroma .nn { } 30 | /* NameOther */ .chroma .nx { } 31 | /* NameProperty */ .chroma .py { } 32 | /* NameTag */ .chroma .nt { color: #ff6ac1 } 33 | /* NameVariable */ .chroma .nv { color: #ff5c57 } 34 | /* NameVariableClass */ .chroma .vc { color: #ff5c57 } 35 | /* NameVariableGlobal */ .chroma .vg { color: #ff5c57 } 36 | /* NameVariableInstance */ .chroma .vi { color: #ff5c57 } 37 | /* NameVariableMagic */ .chroma .vm { } 38 | /* Literal */ .chroma .l { } 39 | /* LiteralDate */ .chroma .ld { } 40 | /* LiteralString */ .chroma .s { color: #5af78e } 41 | /* LiteralStringAffix */ .chroma .sa { color: #5af78e } 42 | /* LiteralStringBacktick */ .chroma .sb { color: #5af78e } 43 | /* LiteralStringChar */ .chroma .sc { color: #5af78e } 44 | /* LiteralStringDelimiter */ .chroma .dl { color: #5af78e } 45 | /* LiteralStringDoc */ .chroma .sd { color: #5af78e } 46 | /* LiteralStringDouble */ .chroma .s2 { color: #5af78e } 47 | /* LiteralStringEscape */ .chroma .se { color: #5af78e } 48 | /* LiteralStringHeredoc */ .chroma .sh { color: #5af78e } 49 | /* LiteralStringInterpol */ .chroma .si { color: #5af78e } 50 | /* LiteralStringOther */ .chroma .sx { color: #5af78e } 51 | /* LiteralStringRegex */ .chroma .sr { color: #5af78e } 52 | /* LiteralStringSingle */ .chroma .s1 { color: #5af78e } 53 | /* LiteralStringSymbol */ .chroma .ss { color: #5af78e } 54 | /* LiteralNumber */ .chroma .m { color: #ff9f43 } 55 | /* LiteralNumberBin */ .chroma .mb { color: #ff9f43 } 56 | /* LiteralNumberFloat */ .chroma .mf { color: #ff9f43 } 57 | /* LiteralNumberHex */ .chroma .mh { color: #ff9f43 } 58 | /* LiteralNumberInteger */ .chroma .mi { color: #ff9f43 } 59 | /* LiteralNumberIntegerLong */ .chroma .il { color: #ff9f43 } 60 | /* LiteralNumberOct */ .chroma .mo { color: #ff9f43 } 61 | /* Operator */ .chroma .o { color: #ff6ac1 } 62 | /* OperatorWord */ .chroma .ow { color: #ff6ac1 } 63 | /* Punctuation */ .chroma .p { } 64 | /* Comment */ .chroma .c { color: #78787e } 65 | /* CommentHashbang */ .chroma .ch { color: #78787e } 66 | /* CommentMultiline */ .chroma .cm { color: #78787e } 67 | /* CommentSingle */ .chroma .c1 { color: #78787e } 68 | /* CommentSpecial */ .chroma .cs { color: #78787e } 69 | /* CommentPreproc */ .chroma .cp { color: #78787e } 70 | /* CommentPreprocFile */ .chroma .cpf { color: #78787e } 71 | /* Generic */ .chroma .g { } 72 | /* GenericDeleted */ .chroma .gd { color: #ff5c57 } 73 | /* GenericEmph */ .chroma .ge { text-decoration: underline } 74 | /* GenericError */ .chroma .gr { color: #ff5c57 } 75 | /* GenericHeading */ .chroma .gh { font-weight: bold } 76 | /* GenericInserted */ .chroma .gi { font-weight: bold } 77 | /* GenericOutput */ .chroma .go { color: #43454f } 78 | /* GenericPrompt */ .chroma .gp { } 79 | /* GenericStrong */ .chroma .gs { font-style: italic } 80 | /* GenericSubheading */ .chroma .gu { font-weight: bold } 81 | /* GenericTraceback */ .chroma .gt { } 82 | /* GenericUnderline */ .chroma .gl { text-decoration: underline } 83 | /* TextWhitespace */ .chroma .w { } 84 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/css/chroma-neon.css: -------------------------------------------------------------------------------- 1 | /* based on rrt 2 | /* Background */ .chroma { color: #f8f8f2; background-color: #000000 } 3 | /* Other */ .chroma .x { } 4 | /* Error */ .chroma .err { } 5 | /* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; } 6 | /* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; width: auto; overflow: auto; display: block; } 7 | /* LineHighlight */ .chroma .hl { display: block; width: 100%;background-color: #ffffcc } 8 | /* LineNumbersTable */ .chroma .lnt { margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7c7c79 } 9 | /* LineNumbers */ .chroma .ln { margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7c7c79 } 10 | /* Keyword */ .chroma .k { color: #ff0000 } 11 | /* KeywordConstant */ .chroma .kc { color: #ff0000 } 12 | /* KeywordDeclaration */ .chroma .kd { color: #ff0000 } 13 | /* KeywordNamespace */ .chroma .kn { color: #ff0000 } 14 | /* KeywordPseudo */ .chroma .kp { color: #ff0000 } 15 | /* KeywordReserved */ .chroma .kr { color: #ff0000 } 16 | /* KeywordType */ .chroma .kt { color: #ee82ee } 17 | /* Name */ .chroma .n { } 18 | /* NameAttribute */ .chroma .na { } 19 | /* NameBuiltin */ .chroma .nb { } 20 | /* NameBuiltinPseudo */ .chroma .bp { } 21 | /* NameClass */ .chroma .nc { } 22 | /* NameConstant */ .chroma .no { color: #7fffd4 } 23 | /* NameDecorator */ .chroma .nd { } 24 | /* NameEntity */ .chroma .ni { } 25 | /* NameException */ .chroma .ne { } 26 | /* NameFunction */ .chroma .nf { color: #ffff00 } 27 | /* NameFunctionMagic */ .chroma .fm { } 28 | /* NameLabel */ .chroma .nl { } 29 | /* NameNamespace */ .chroma .nn { } 30 | /* NameOther */ .chroma .nx { } 31 | /* NameProperty */ .chroma .py { } 32 | /* NameTag */ .chroma .nt { } 33 | /* NameVariable */ .chroma .nv { color: #eedd82 } 34 | /* NameVariableClass */ .chroma .vc { } 35 | /* NameVariableGlobal */ .chroma .vg { } 36 | /* NameVariableInstance */ .chroma .vi { } 37 | /* NameVariableMagic */ .chroma .vm { } 38 | /* Literal */ .chroma .l { } 39 | /* LiteralDate */ .chroma .ld { } 40 | /* LiteralString */ .chroma .s { color: #87ceeb } 41 | /* LiteralStringAffix */ .chroma .sa { color: #87ceeb } 42 | /* LiteralStringBacktick */ .chroma .sb { color: #87ceeb } 43 | /* LiteralStringChar */ .chroma .sc { color: #87ceeb } 44 | /* LiteralStringDelimiter */ .chroma .dl { color: #87ceeb } 45 | /* LiteralStringDoc */ .chroma .sd { color: #87ceeb } 46 | /* LiteralStringDouble */ .chroma .s2 { color: #87ceeb } 47 | /* LiteralStringEscape */ .chroma .se { color: #87ceeb } 48 | /* LiteralStringHeredoc */ .chroma .sh { color: #87ceeb } 49 | /* LiteralStringInterpol */ .chroma .si { color: #87ceeb } 50 | /* LiteralStringOther */ .chroma .sx { color: #87ceeb } 51 | /* LiteralStringRegex */ .chroma .sr { color: #87ceeb } 52 | /* LiteralStringSingle */ .chroma .s1 { color: #87ceeb } 53 | /* LiteralStringSymbol */ .chroma .ss { color: #ff6600 } 54 | /* LiteralNumber */ .chroma .m { color: #ff6600 } 55 | /* LiteralNumberBin */ .chroma .mb { color: #ff6600 } 56 | /* LiteralNumberFloat */ .chroma .mf { color: #ff6600 } 57 | /* LiteralNumberHex */ .chroma .mh { color: #ff6600 } 58 | /* LiteralNumberInteger */ .chroma .mi { color: #ff6600 } 59 | /* LiteralNumberIntegerLong */ .chroma .il { color: #ff6600 } 60 | /* LiteralNumberOct */ .chroma .mo { color: #ff6600 } 61 | /* Operator */ .chroma .o { } 62 | /* OperatorWord */ .chroma .ow { } 63 | /* Punctuation */ .chroma .p { } 64 | /* Comment */ .chroma .c { color: #00ff00 } 65 | /* CommentHashbang */ .chroma .ch { color: #00ff00 } 66 | /* CommentMultiline */ .chroma .cm { color: #00ff00 } 67 | /* CommentSingle */ .chroma .c1 { color: #00ff00 } 68 | /* CommentSpecial */ .chroma .cs { color: #00ff00 } 69 | /* CommentPreproc */ .chroma .cp { color: #e5e5e5 } 70 | /* CommentPreprocFile */ .chroma .cpf { color: #e5e5e5 } 71 | /* Generic */ .chroma .g { } 72 | /* GenericDeleted */ .chroma .gd { } 73 | /* GenericEmph */ .chroma .ge { } 74 | /* GenericError */ .chroma .gr { } 75 | /* GenericHeading */ .chroma .gh { } 76 | /* GenericInserted */ .chroma .gi { } 77 | /* GenericOutput */ .chroma .go { } 78 | /* GenericPrompt */ .chroma .gp { } 79 | /* GenericStrong */ .chroma .gs { } 80 | /* GenericSubheading */ .chroma .gu { } 81 | /* GenericTraceback */ .chroma .gt { } 82 | /* GenericUnderline */ .chroma .gl { } 83 | /* TextWhitespace */ .chroma .w { } 84 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/css/chroma-relearn-dark.css: -------------------------------------------------------------------------------- 1 | /* based on monokai 2 | /* Background */ .chroma { color: #f8f8f8; background-color: #2b2b2b } 3 | /* Other */ .chroma .x { } 4 | /* Error */ .chroma .err { color: #960050; } 5 | /* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; } 6 | /* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; width: auto; overflow: auto; display: block; } 7 | /* LineHighlight */ .chroma .hl { display: block; width: 100%;background-color: #ffffcc } 8 | /* LineNumbersTable */ .chroma .lnt { margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f } 9 | /* LineNumbers */ .chroma .ln { margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f } 10 | /* Keyword */ .chroma .k { color: #66d9ef } 11 | /* KeywordConstant */ .chroma .kc { color: #66d9ef } 12 | /* KeywordDeclaration */ .chroma .kd { color: #66d9ef } 13 | /* KeywordNamespace */ .chroma .kn { color: #f92672 } 14 | /* KeywordPseudo */ .chroma .kp { color: #66d9ef } 15 | /* KeywordReserved */ .chroma .kr { color: #66d9ef } 16 | /* KeywordType */ .chroma .kt { color: #66d9ef } 17 | /* Name */ .chroma .n { } 18 | /* NameAttribute */ .chroma .na { color: #a6e22e } 19 | /* NameBuiltin */ .chroma .nb { } 20 | /* NameBuiltinPseudo */ .chroma .bp { } 21 | /* NameClass */ .chroma .nc { color: #a6e22e } 22 | /* NameConstant */ .chroma .no { color: #66d9ef } 23 | /* NameDecorator */ .chroma .nd { color: #a6e22e } 24 | /* NameEntity */ .chroma .ni { } 25 | /* NameException */ .chroma .ne { color: #a6e22e } 26 | /* NameFunction */ .chroma .nf { color: #a6e22e } 27 | /* NameFunctionMagic */ .chroma .fm { } 28 | /* NameLabel */ .chroma .nl { } 29 | /* NameNamespace */ .chroma .nn { } 30 | /* NameOther */ .chroma .nx { color: #a6e22e } 31 | /* NameProperty */ .chroma .py { } 32 | /* NameTag */ .chroma .nt { color: #f92672 } 33 | /* NameVariable */ .chroma .nv { } 34 | /* NameVariableClass */ .chroma .vc { } 35 | /* NameVariableGlobal */ .chroma .vg { } 36 | /* NameVariableInstance */ .chroma .vi { } 37 | /* NameVariableMagic */ .chroma .vm { } 38 | /* Literal */ .chroma .l { color: #ae81ff } 39 | /* LiteralDate */ .chroma .ld { color: #e6db74 } 40 | /* LiteralString */ .chroma .s { color: #e6db74 } 41 | /* LiteralStringAffix */ .chroma .sa { color: #e6db74 } 42 | /* LiteralStringBacktick */ .chroma .sb { color: #e6db74 } 43 | /* LiteralStringChar */ .chroma .sc { color: #e6db74 } 44 | /* LiteralStringDelimiter */ .chroma .dl { color: #e6db74 } 45 | /* LiteralStringDoc */ .chroma .sd { color: #e6db74 } 46 | /* LiteralStringDouble */ .chroma .s2 { color: #e6db74 } 47 | /* LiteralStringEscape */ .chroma .se { color: #ae81ff } 48 | /* LiteralStringHeredoc */ .chroma .sh { color: #e6db74 } 49 | /* LiteralStringInterpol */ .chroma .si { color: #e6db74 } 50 | /* LiteralStringOther */ .chroma .sx { color: #e6db74 } 51 | /* LiteralStringRegex */ .chroma .sr { color: #e6db74 } 52 | /* LiteralStringSingle */ .chroma .s1 { color: #e6db74 } 53 | /* LiteralStringSymbol */ .chroma .ss { color: #e6db74 } 54 | /* LiteralNumber */ .chroma .m { color: #ae81ff } 55 | /* LiteralNumberBin */ .chroma .mb { color: #ae81ff } 56 | /* LiteralNumberFloat */ .chroma .mf { color: #ae81ff } 57 | /* LiteralNumberHex */ .chroma .mh { color: #ae81ff } 58 | /* LiteralNumberInteger */ .chroma .mi { color: #ae81ff } 59 | /* LiteralNumberIntegerLong */ .chroma .il { color: #ae81ff } 60 | /* LiteralNumberOct */ .chroma .mo { color: #ae81ff } 61 | /* Operator */ .chroma .o { color: #f92672 } 62 | /* OperatorWord */ .chroma .ow { color: #f92672 } 63 | /* Punctuation */ .chroma .p { } 64 | /* Comment */ .chroma .c { color: #7c7c7c } 65 | /* CommentHashbang */ .chroma .ch { color: #7c7c7c } 66 | /* CommentMultiline */ .chroma .cm { color: #7c7c7c } 67 | /* CommentSingle */ .chroma .c1 { color: #7c7c7c } 68 | /* CommentSpecial */ .chroma .cs { color: #7c7c7c } 69 | /* CommentPreproc */ .chroma .cp { color: #7c7c7c } 70 | /* CommentPreprocFile */ .chroma .cpf { color: #7c7c7c } 71 | /* Generic */ .chroma .g { } 72 | /* GenericDeleted */ .chroma .gd { color: #f92672 } 73 | /* GenericEmph */ .chroma .ge { font-style: italic } 74 | /* GenericError */ .chroma .gr { } 75 | /* GenericHeading */ .chroma .gh { } 76 | /* GenericInserted */ .chroma .gi { color: #a6e22e } 77 | /* GenericOutput */ .chroma .go { } 78 | /* GenericPrompt */ .chroma .gp { } 79 | /* GenericStrong */ .chroma .gs { font-weight: bold } 80 | /* GenericSubheading */ .chroma .gu { color: #7c7c7c } 81 | /* GenericTraceback */ .chroma .gt { } 82 | /* GenericUnderline */ .chroma .gl { } 83 | /* TextWhitespace */ .chroma .w { } 84 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/css/featherlight.min.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Featherlight - ultra slim jQuery lightbox 3 | * Version 1.7.13 - http://noelboss.github.io/featherlight/ 4 | * 5 | * Copyright 2018, Noël Raoul Bossart (http://www.noelboss.com) 6 | * MIT Licensed. 7 | **/ 8 | html.with-featherlight{overflow:hidden}.featherlight{display:none;position:fixed;top:0;right:0;bottom:0;left:0;z-index:2147483647;text-align:center;white-space:nowrap;cursor:pointer;background:#333;background:rgba(0,0,0,0)}.featherlight:last-of-type{background:rgba(0,0,0,.8)}.featherlight:before{content:'';display:inline-block;height:100%;vertical-align:middle}.featherlight .featherlight-content{position:relative;text-align:left;vertical-align:middle;display:inline-block;overflow:auto;padding:25px 25px 0;border-bottom:25px solid transparent;margin-left:5%;margin-right:5%;max-height:95%;background:#fff;cursor:auto;white-space:normal}.featherlight .featherlight-inner{display:block}.featherlight link.featherlight-inner,.featherlight script.featherlight-inner,.featherlight style.featherlight-inner{display:none}.featherlight .featherlight-close-icon{position:absolute;z-index:9999;top:0;right:0;line-height:25px;width:25px;cursor:pointer;text-align:center;font-family:Arial,sans-serif;background:#fff;background:rgba(255,255,255,.3);color:#000;border:0;padding:0}.featherlight .featherlight-close-icon::-moz-focus-inner{border:0;padding:0}.featherlight .featherlight-image{width:100%}.featherlight-iframe .featherlight-content{border-bottom:0;padding:0;-webkit-overflow-scrolling:touch}.featherlight iframe{border:0}.featherlight *{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}@media only screen and (max-width:1024px){.featherlight .featherlight-content{margin-left:0;margin-right:0;max-height:98%;padding:10px 10px 0;border-bottom:10px solid transparent}}@media print{html.with-featherlight>*>:not(.featherlight){display:none}} -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/css/perfect-scrollbar.min.css: -------------------------------------------------------------------------------- 1 | .ps{overflow:hidden!important;overflow-anchor:none;-ms-overflow-style:none;touch-action:auto;-ms-touch-action:auto}.ps__rail-x{display:none;opacity:0;transition:background-color .2s linear,opacity .2s linear;-webkit-transition:background-color .2s linear,opacity .2s linear;height:15px;bottom:0;position:absolute}.ps__rail-y{display:none;opacity:0;transition:background-color .2s linear,opacity .2s linear;-webkit-transition:background-color .2s linear,opacity .2s linear;width:15px;right:0;position:absolute}.ps--active-x>.ps__rail-x,.ps--active-y>.ps__rail-y{display:block;background-color:transparent}.ps--focus>.ps__rail-x,.ps--focus>.ps__rail-y,.ps--scrolling-x>.ps__rail-x,.ps--scrolling-y>.ps__rail-y,.ps:hover>.ps__rail-x,.ps:hover>.ps__rail-y{opacity:.6}.ps .ps__rail-x.ps--clicking,.ps .ps__rail-x:focus,.ps .ps__rail-x:hover,.ps .ps__rail-y.ps--clicking,.ps .ps__rail-y:focus,.ps .ps__rail-y:hover{background-color:#eee;opacity:.9}.ps__thumb-x{background-color:#aaa;border-radius:6px;transition:background-color .2s linear,height .2s ease-in-out;-webkit-transition:background-color .2s linear,height .2s ease-in-out;height:6px;bottom:2px;position:absolute}.ps__thumb-y{background-color:#aaa;border-radius:6px;transition:background-color .2s linear,width .2s ease-in-out;-webkit-transition:background-color .2s linear,width .2s ease-in-out;width:6px;right:2px;position:absolute}.ps__rail-x.ps--clicking .ps__thumb-x,.ps__rail-x:focus>.ps__thumb-x,.ps__rail-x:hover>.ps__thumb-x{background-color:#999;height:11px}.ps__rail-y.ps--clicking .ps__thumb-y,.ps__rail-y:focus>.ps__thumb-y,.ps__rail-y:hover>.ps__thumb-y{background-color:#999;width:11px}@supports (-ms-overflow-style:none){.ps{overflow:auto!important}}@media screen and (-ms-high-contrast:active),(-ms-high-contrast:none){.ps{overflow:auto!important}} -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/css/print.css: -------------------------------------------------------------------------------- 1 | @import "theme-relearn-light.css"; 2 | 3 | #sidebar { 4 | display: none; 5 | } 6 | #body { 7 | margin-left: 0; 8 | min-width: 100%; 9 | max-width: 100%; 10 | width: 100%; 11 | } 12 | #body #navigation { 13 | display: none; 14 | } 15 | html, 16 | body #body{ 17 | font-size: 8.9pt; 18 | } 19 | body { 20 | background-color: white; 21 | } 22 | pre code { 23 | font-size: 8.3pt; 24 | } 25 | code.copy-to-clipboard-code { 26 | border-bottom-right-radius: 2px; 27 | border-top-right-radius: 2px; 28 | border-right-width: 1px; 29 | } 30 | pre { 31 | border: 1px solid #ccc; 32 | } 33 | #body #topbar{ 34 | background-color: #fff; /* avoid background bleeding*/ 35 | border-bottom: 1px solid #ddd; 36 | border-radius: 0; 37 | padding-left: 0; /* for print, we want to align with the footer to ease the layout */ 38 | color: #777; 39 | } 40 | .navigation, 41 | #top-print-link, 42 | #top-github-link { 43 | /* we don't need this while printing */ 44 | display: none; 45 | } 46 | #body #breadcrumbs { 47 | width: 100%; 48 | } 49 | #body #breadcrumbs .links { 50 | overflow-x: hidden; 51 | visibility: visible; 52 | } 53 | .copy-to-clipboard-button { 54 | display: none; 55 | } 56 | 57 | #body h1, #body h2, #body h3, #body h4, #body h5, #body h6 { 58 | /* better contrast for colored elements */ 59 | color: black; 60 | } 61 | #body th, #body td, 62 | #body code, #body strong, #body b, 63 | #body li, #body dd, #body dt, 64 | #body p, 65 | #body .anchor, 66 | #body a { 67 | /* better contrast for colored elements */ 68 | color: black; 69 | } 70 | #body pre, 71 | #body code { 72 | background-color: white; 73 | border-color: #ddd; 74 | } 75 | 76 | hr{ 77 | border-bottom: 1px solid #ddd; 78 | } 79 | body, 80 | #body, 81 | #body-inner { 82 | overflow: visible !important; /* turn off limitations for perfect scrollbar */ 83 | } 84 | #body #body-inner { 85 | /* reset paddings for chapters in screen */ 86 | padding: 0 3rem 4rem 3rem; 87 | } 88 | 89 | #body #body-inner h1 { 90 | border-bottom: 1px solid #ddd; 91 | margin-bottom: 2rem; 92 | padding-bottom: .75rem; 93 | } 94 | #body-inner .chapter h3:first-of-type { 95 | margin-top: 2rem; 96 | } 97 | #body-inner .chapter p { 98 | font-size: 1rem; 99 | } 100 | 101 | .footline { 102 | /* in print mode show footer line to signal reader the end of document */ 103 | border-top: 1px solid #ddd; 104 | color: #777; 105 | margin-top: 1.5rem; 106 | padding-top: .75rem; 107 | } 108 | #body #body-inner .footline a { 109 | text-decoration: none; 110 | } 111 | #body #body-inner a { 112 | /* in print we want to distinguish links in our content from 113 | normal text even if printed black/white; 114 | don't use a.highlight in selector to also get links that are 115 | put as HTML into markdown */ 116 | text-decoration-line: underline; 117 | } 118 | #toc-menu { 119 | /* we don't need this while printing */ 120 | display: none; 121 | } 122 | #body #sidebar-toggle-span { 123 | /* we don't need this while printing */ 124 | display: none; 125 | } 126 | #breadcrumbs .links { 127 | display: inline; 128 | } 129 | #topbar{ 130 | /* the header is sticky which is not suitable for print; */ 131 | position: inherit; /* IE11 doesn't know "initial" here */ 132 | } 133 | #topbar > div { 134 | background-color: #ffffff; /* IE11 doesn't know "initial" here */ 135 | } 136 | #body .tab-nav-button:not(.active) { 137 | opacity: .5; 138 | } 139 | #head-tags { 140 | display: none; 141 | } 142 | mark { 143 | background: inherit; 144 | color: inherit; 145 | } 146 | .mermaid > svg:hover { 147 | border-color: transparent; 148 | } 149 | div.box { 150 | border: 1px solid #ddd; 151 | } 152 | div.box > .box-content { 153 | background-color: white; 154 | } 155 | rapi-doc{ 156 | /* adjust rapi-doc internals to fill out available space */ 157 | font-size: 4pt; 158 | margin-left: -12px; 159 | width: calc( 100% + 12px + 8px ); 160 | } 161 | .btn-default, 162 | #body .tab-nav-button { 163 | color: black !important; 164 | } 165 | #body .tab-nav-button.active { 166 | background-color: white !important; 167 | border-bottom-color: white !important; 168 | color: black; 169 | } 170 | #body .tab-nav-button:not(.active) { 171 | opacity: 1; 172 | } 173 | 174 | article { 175 | break-before: page; 176 | } 177 | #body-inner article:first-of-type { 178 | break-before: avoid; 179 | } 180 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/css/tabs.css: -------------------------------------------------------------------------------- 1 | #body .tab-nav-button { 2 | background-color: rgba( 134, 134, 134, .166 ) !important; 3 | border-color: rgba( 134, 134, 134, .333 ) !important; 4 | border-radius: 4px 4px 0 0 !important; 5 | border-width: 1px 1px 1px 1px !important; 6 | bottom: -1px; 7 | -webkit-print-color-adjust: exact; 8 | color-adjust: exact; 9 | display: block; 10 | float: left; 11 | margin-left: 4px; 12 | position: relative; 13 | } 14 | #body .tab-nav-button:first-child { 15 | margin-left: 9px; 16 | } 17 | #body .tab-nav-button.active { 18 | background-color: #ffffff !important; /* var(--MAIN-BG-color) */ 19 | border-bottom-color: #ffffff !important; /* var(--MAIN-BG-color) */ 20 | } 21 | #body .tab-nav-button:not(.active) { 22 | border-bottom-color: rgba( 134, 134, 134, .1 ) !important; 23 | margin-top: 7px; 24 | padding-bottom: 2px !important; 25 | padding-top: 2px !important; 26 | } 27 | #body .tab-nav-button:not(.active) span { 28 | opacity: .8; 29 | } 30 | #body .tab-panel { 31 | margin-bottom: 1.5rem; 32 | margin-top: 1.5rem; 33 | } 34 | #body .tab-content { 35 | background-color: transparent; 36 | border-color: rgba( 134, 134, 134, .333 ); 37 | border-style: solid; 38 | border-width: 1px; 39 | clear: both; 40 | -webkit-print-color-adjust: exact; 41 | color-adjust: exact; 42 | display: block; 43 | padding: 8px; 44 | z-index: 10; 45 | } 46 | #body .tab-content .tab-item{ 47 | display: none; 48 | } 49 | 50 | #body .tab-content .tab-item.active{ 51 | display: block; 52 | } 53 | 54 | #body .tab-item pre{ 55 | margin-bottom: 0; 56 | margin-top: 0; 57 | } 58 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/css/tags.css: -------------------------------------------------------------------------------- 1 | /* Tags */ 2 | 3 | #head-tags{ 4 | margin-left:1rem; 5 | margin-top:1rem; 6 | } 7 | 8 | #body .tags a.tag-link { 9 | background: #7dc903; /* var(--TAG-BG-color) */ 10 | border-bottom-right-radius: 3px; 11 | border-top-right-radius: 3px; 12 | box-shadow: 0 1px 2px rgba(0,0,0,0.2); 13 | color: #ffffff; /* var(--MAIN-BG-color) */ 14 | display: inline-block; 15 | font-size: 0.8em; 16 | font-weight: 400; 17 | line-height: 2em; 18 | margin: 0 16px 8px 0; 19 | padding: 0 10px 0 12px; 20 | position: relative; 21 | } 22 | 23 | #body .tags a.tag-link:before { 24 | border-color: transparent #7dc903 transparent transparent; /* var(--TAG-BG-color) */ 25 | border-style: solid; 26 | border-width: 1em 1em 1em 0; 27 | content: ""; 28 | left: -.99em; 29 | height: 0; 30 | position: absolute; 31 | top:0; 32 | width: 0; 33 | } 34 | 35 | #body .tags a.tag-link:after { 36 | background: #ffffff; /* var(--MAIN-BG-color) */ 37 | border-radius: 100%; 38 | content: ""; 39 | left: 1px; 40 | height: 5px; 41 | position: absolute; 42 | top: 10px; 43 | width: 5px; 44 | } 45 | 46 | #body .tags a.tag-link:hover:after { 47 | width: 5px; 48 | } 49 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/css/theme-blue.css: -------------------------------------------------------------------------------- 1 | /* here in this showcase we use our own modified chroma syntax highlightning style; 2 | if you want to use a predefined style instead: 3 | - remove `markup.highlight.noClasses` from your config.toml 4 | - set `markup.highlight.style` to a predefined style name in your config.toml 5 | - remove the following `@import` of the self-defined chroma stylesheet */ 6 | @import "chroma-learn.css"; 7 | 8 | :root { 9 | --MAIN-TEXT-color: #323232; /* Color of text by default */ 10 | --MAIN-TITLES-TEXT-color: #5e5e5e; /* Color of titles h2-h3-h4-h5-h6 */ 11 | --MAIN-LINK-color: #1C90F3; /* Color of links */ 12 | --MAIN-LINK-HOVER-color: #167ad0; /* Color of hovered links */ 13 | --MAIN-ANCHOR-color: #1C90F3; /* color of anchors on titles */ 14 | --MAIN-BG-color: #ffffff; /* color of text by default */ 15 | 16 | /* adjusted to base16-snazzy chroma style */ 17 | --CODE-BLOCK-color: #e2e4e5; /* fallback color for code text */ 18 | --CODE-BLOCK-BG-color: #282a36; /* fallback color for code background */ 19 | --CODE-BLOCK-BORDER-color: #282a36; /* color of block code border */ 20 | 21 | --CODE-INLINE-color: #5e5e5e; /* color for inline code text */ 22 | --CODE-INLINE-BG-color: #fffae9; /* color for inline code background */ 23 | --CODE-INLINE-BORDER-color: #f8e8c8; /* color of inline code border */ 24 | 25 | --MENU-HOME-LINK-color: #323232; /* Color of the home button text */ 26 | --MENU-HOME-LINK-HOVER-color: #5e5e5e; /* Color of the hovered home button text */ 27 | 28 | --MENU-HEADER-BG-color: #1C90F3; /* Background color of menu header */ 29 | --MENU-HEADER-BORDER-color: #33a1ff; /*Color of menu header border */ 30 | 31 | --MENU-SEARCH-color: #ffffff; /* Color of search field text */ 32 | --MENU-SEARCH-BG-color: #167ad0; /* Search field background color (by default borders + icons) */ 33 | --MENU-SEARCH-BORDER-color: #33a1ff; /* Override search field border color */ 34 | 35 | --MENU-SECTIONS-ACTIVE-BG-color: #20272b; /* Background color of the active section and its children */ 36 | --MENU-SECTIONS-BG-color: #252c31; /* Background color of other sections */ 37 | --MENU-SECTIONS-LINK-color: #ccc; /* Color of links in menu */ 38 | --MENU-SECTIONS-LINK-HOVER-color: #e6e6e6; /* Color of links in menu, when hovered */ 39 | --MENU-SECTION-ACTIVE-CATEGORY-color: #777; /* Color of active category text */ 40 | --MENU-SECTION-ACTIVE-CATEGORY-BG-color: #fff; /* Color of background for the active category (only) */ 41 | 42 | --MENU-VISITED-color: #33a1ff; /* Color of 'page visited' icons in menu */ 43 | --MENU-SECTION-HR-color: #20272b; /* Color of
separator in menu */ 44 | 45 | /* base styling for boxes */ 46 | --BOX-CAPTION-color: rgba( 255, 255, 255, 1 ); /* color of the title text */ 47 | --BOX-BG-color: rgba( 255, 255, 255, .833 ); /* color of the content background */ 48 | --BOX-TEXT-color: rgba( 16, 16, 16, 1 ); /* fixed color of the content text */ 49 | } 50 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/css/theme-green.css: -------------------------------------------------------------------------------- 1 | /* here in this showcase we use our own modified chroma syntax highlightning style; 2 | if you want to use a predefined style instead: 3 | - remove `markup.highlight.noClasses` from your config.toml 4 | - set `markup.highlight.style` to a predefined style name in your config.toml 5 | - remove the following `@import` of the self-defined chroma stylesheet */ 6 | @import "chroma-learn.css"; 7 | 8 | :root { 9 | --MAIN-TEXT-color: #323232; /* Color of text by default */ 10 | --MAIN-TITLES-TEXT-color: #5e5e5e; /* Color of titles h2-h3-h4-h5-h6 */ 11 | --MAIN-LINK-color: #599a3e; /* Color of links */ 12 | --MAIN-LINK-HOVER-color: #3f6d2c; /* Color of hovered links */ 13 | --MAIN-ANCHOR-color: #599a3e; /* color of anchors on titles */ 14 | --MAIN-BG-color: #ffffff; /* color of text by default */ 15 | 16 | /* adjusted to base16-snazzy chroma style */ 17 | --CODE-BLOCK-color: #e2e4e5; /* fallback color for code text */ 18 | --CODE-BLOCK-BG-color: #282a36; /* fallback color for code background */ 19 | --CODE-BLOCK-BORDER-color: #282a36; /* color of block code border */ 20 | 21 | --CODE-INLINE-color: #5e5e5e; /* color for inline code text */ 22 | --CODE-INLINE-BG-color: #fffae9; /* color for inline code background */ 23 | --CODE-INLINE-BORDER-color: #f8e8c8; /* color of inline code border */ 24 | 25 | --MENU-HOME-LINK-color: #323232; /* Color of the home button text */ 26 | --MENU-HOME-LINK-HOVER-color: #5e5e5e; /* Color of the hovered home button text */ 27 | 28 | --MENU-HEADER-BG-color: #74b559; /* Background color of menu header */ 29 | --MENU-HEADER-BORDER-color: #9cd484; /*Color of menu header border */ 30 | 31 | --MENU-SEARCH-color: #ffffff; /* Color of search field text */ 32 | --MENU-SEARCH-BG-color: #599a3e; /* Search field background color (by default borders + icons) */ 33 | --MENU-SEARCH-BORDER-color: #84c767; /* Override search field border color */ 34 | 35 | --MENU-SECTIONS-ACTIVE-BG-color: #1b211c; /* Background color of the active section and its children */ 36 | --MENU-SECTIONS-BG-color: #222723; /* Background color of other sections */ 37 | --MENU-SECTIONS-LINK-color: #ccc; /* Color of links in menu */ 38 | --MENU-SECTIONS-LINK-HOVER-color: #e6e6e6; /* Color of links in menu, when hovered */ 39 | --MENU-SECTION-ACTIVE-CATEGORY-color: #777; /* Color of active category text */ 40 | --MENU-SECTION-ACTIVE-CATEGORY-BG-color: #fff; /* Color of background for the active category (only) */ 41 | 42 | --MENU-VISITED-color: #599a3e; /* Color of 'page visited' icons in menu */ 43 | --MENU-SECTION-HR-color: #18211c; /* Color of
separator in menu */ 44 | 45 | /* base styling for boxes */ 46 | --BOX-CAPTION-color: rgba( 255, 255, 255, 1 ); /* color of the title text */ 47 | --BOX-BG-color: rgba( 255, 255, 255, .833 ); /* color of the content background */ 48 | --BOX-TEXT-color: rgba( 16, 16, 16, 1 ); /* fixed color of the content text */ 49 | } 50 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/css/theme-learn.css: -------------------------------------------------------------------------------- 1 | /* here in this showcase we use our own modified chroma syntax highlightning style; 2 | if you want to use a predefined style instead: 3 | - remove `markup.highlight.noClasses` from your config.toml 4 | - set `markup.highlight.style` to a predefined style name in your config.toml 5 | - remove the following `@import` of the self-defined chroma stylesheet */ 6 | @import "chroma-learn.css"; 7 | 8 | :root { 9 | --MAIN-TEXT-color: #323232; /* Color of text by default */ 10 | --MAIN-TITLES-TEXT-color: #5e5e5e; /* Color of titles h2-h3-h4-h5-h6 */ 11 | --MAIN-LINK-color: #00bdf3; /* Color of links */ 12 | --MAIN-LINK-HOVER-color: #0082a7; /* Color of hovered links */ 13 | --MAIN-ANCHOR-color: #00bdf3; /* color of anchors on titles */ 14 | --MAIN-BG-color: #ffffff; /* color of text by default */ 15 | 16 | /* adjusted to base16-snazzy chroma style */ 17 | --CODE-BLOCK-color: #e2e4e5; /* fallback color for code text */ 18 | --CODE-BLOCK-BG-color: #282a36; /* fallback color for code background */ 19 | --CODE-BLOCK-BORDER-color: #282a36; /* color of block code border */ 20 | 21 | --CODE-INLINE-color: #5e5e5e; /* color for inline code text */ 22 | --CODE-INLINE-BG-color: #fff7dd; /* color for inline code background */ 23 | --CODE-INLINE-BORDER-color: #fbf0cb; /* color of inline code border */ 24 | 25 | --MENU-HOME-LINK-color: #cccccc; /* Color of the home button text */ 26 | --MENU-HOME-LINK-HOVER-color: #e6e6e6; /* Color of the hovered home button text */ 27 | 28 | --MENU-HEADER-BG-color: #8451a1; /* Background color of menu header */ 29 | --MENU-HEADER-BORDER-color: #9c6fb6; /*Color of menu header border */ 30 | 31 | --MENU-SEARCH-color: #ffffff; /* Color of search field text */ 32 | --MENU-SEARCH-BG-color: #764890; /* Search field background color (by default borders + icons) */ 33 | --MENU-SEARCH-BORDER-color: #915eae; /* Override search field border color */ 34 | 35 | --MENU-SECTIONS-ACTIVE-BG-color: #251f29; /* Background color of the active section and its children */ 36 | --MENU-SECTIONS-BG-color: #322a38; /* Background color of other sections */ 37 | --MENU-SECTIONS-LINK-color: #cccccc; /* Color of links in menu */ 38 | --MENU-SECTIONS-LINK-HOVER-color: #e6e6e6; /* Color of links in menu, when hovered */ 39 | --MENU-SECTION-ACTIVE-CATEGORY-color: #777777; /* Color of active category text */ 40 | --MENU-SECTION-ACTIVE-CATEGORY-BG-color: #ffffff; /* Color of background for the active category (only) */ 41 | 42 | --MENU-VISITED-color: #00bdf3; /* Color of 'page visited' icons in menu */ 43 | --MENU-SECTION-HR-color: #2a232f; /* Color of
separator in menu */ 44 | 45 | /* base styling for boxes */ 46 | --BOX-CAPTION-color: rgba( 255, 255, 255, 1 ); /* color of the title text */ 47 | --BOX-BG-color: rgba( 255, 255, 255, .833 ); /* color of the content background */ 48 | --BOX-TEXT-color: rgba( 16, 16, 16, 1 ); /* fixed color of the content text */ 49 | } 50 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/css/theme-red.css: -------------------------------------------------------------------------------- 1 | /* here in this showcase we use our own modified chroma syntax highlightning style; 2 | if you want to use a predefined style instead: 3 | - remove `markup.highlight.noClasses` from your config.toml 4 | - set `markup.highlight.style` to a predefined style name in your config.toml 5 | - remove the following `@import` of the self-defined chroma stylesheet */ 6 | @import "chroma-learn.css"; 7 | 8 | :root { 9 | --MAIN-TEXT-color: #323232; /* Color of text by default */ 10 | --MAIN-TITLES-TEXT-color: #5e5e5e; /* Color of titles h2-h3-h4-h5-h6 */ 11 | --MAIN-LINK-color: #f31c1c; /* Color of links */ 12 | --MAIN-LINK-HOVER-color: #d01616; /* Color of hovered links */ 13 | --MAIN-ANCHOR-color: #f31c1c; /* color of anchors on titles */ 14 | --MAIN-BG-color: #ffffff; /* color of text by default */ 15 | 16 | /* adjusted to base16-snazzy chroma style */ 17 | --CODE-BLOCK-color: #e2e4e5; /* fallback color for code text */ 18 | --CODE-BLOCK-BG-color: #282a36; /* fallback color for code background */ 19 | --CODE-BLOCK-BORDER-color: #282a36; /* color of block code border */ 20 | 21 | --CODE-INLINE-color: #5e5e5e; /* color for inline code text */ 22 | --CODE-INLINE-BG-color: #fffae9; /* color for inline code background */ 23 | --CODE-INLINE-BORDER-color: #f8e8c8; /* color of inline code border */ 24 | 25 | --MENU-HOME-LINK-color: #ccc; /* Color of the home button text */ 26 | --MENU-HOME-LINK-HOVER-color: #e6e6e6; /* Color of the hovered home button text */ 27 | 28 | --MENU-HEADER-BG-color: #dc1010; /* Background color of menu header */ 29 | --MENU-HEADER-BORDER-color: #e23131; /*Color of menu header border */ 30 | 31 | --MENU-SEARCH-color: #ffffff; /* Color of search field text */ 32 | --MENU-SEARCH-BG-color: #b90000; /* Search field background color (by default borders + icons) */ 33 | --MENU-SEARCH-BORDER-color: #ef2020; /* Override search field border color */ 34 | 35 | --MENU-SECTIONS-ACTIVE-BG-color: #2b2020; /* Background color of the active section and its children */ 36 | --MENU-SECTIONS-BG-color: #312525; /* Background color of other sections */ 37 | --MENU-SECTIONS-LINK-color: #ccc; /* Color of links in menu */ 38 | --MENU-SECTIONS-LINK-HOVER-color: #e6e6e6; /* Color of links in menu, when hovered */ 39 | --MENU-SECTION-ACTIVE-CATEGORY-color: #777; /* Color of active category text */ 40 | --MENU-SECTION-ACTIVE-CATEGORY-BG-color: #fff; /* Color of background for the active category (only) */ 41 | 42 | --MENU-VISITED-color: #ff3333; /* Color of 'page visited' icons in menu */ 43 | --MENU-SECTION-HR-color: #2b2020; /* Color of
separator in menu */ 44 | 45 | /* base styling for boxes */ 46 | --BOX-CAPTION-color: rgba( 255, 255, 255, 1 ); /* color of the title text */ 47 | --BOX-BG-color: rgba( 255, 255, 255, .833 ); /* color of the content background */ 48 | --BOX-TEXT-color: rgba( 16, 16, 16, 1 ); /* fixed color of the content text */ 49 | } 50 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/css/theme-relearn-dark.css: -------------------------------------------------------------------------------- 1 | /* here in this showcase we use our own modified chroma syntax highlightning style; 2 | if you want to use a predefined style instead: 3 | - remove `markup.highlight.noClasses` from your config.toml 4 | - set `markup.highlight.style` to a predefined style name in your config.toml 5 | - remove the following `@import` of the self-defined chroma stylesheet */ 6 | @import "chroma-relearn-dark.css"; 7 | 8 | :root { 9 | --MAIN-TEXT-color: #e0e0e0; /* Color of text by default */ 10 | --MAIN-TITLES-TEXT-color: #ffffff; /* Color of titles h2-h3-h4-h5-h6 */ 11 | --MAIN-LINK-color: #1c90f3; /* Color of links */ 12 | --MAIN-LINK-HOVER-color: #4cabff; /* Color of hovered links */ 13 | --MAIN-ANCHOR-color: #4cabff; /* color of anchors on titles */ 14 | --MAIN-BG-color: #202020; /* color for code background */ 15 | 16 | /* adjusted to relearn-dark chroma style */ 17 | --CODE-BLOCK-color: #f8f8f8; /* fallback color for block code text */ 18 | --CODE-BLOCK-BG-color: #2b2b2b; /* fallback color for block code background */ 19 | --CODE-BLOCK-BORDER-color: #2b2b2b; /* color of block code border */ 20 | 21 | --CODE-INLINE-color: #82e550; /* color for inline code text */ 22 | --CODE-INLINE-BG-color: #2d2d2d; /* color for inline code background */ 23 | --CODE-INLINE-BORDER-color: #464646; /* color of inline code border */ 24 | 25 | --MERMAID-theme: dark; /* name of the default Mermaid theme for this variant, can be overridden in config.toml */ 26 | --SWAGGER-theme: dark; /* name of the default Swagger theme for this variant, can be overridden in config.toml */ 27 | 28 | --MENU-HOME-LINK-color: #323232; /* Color of the home button text */ 29 | --MENU-HOME-LINK-HOVER-color: #5e5e5e; /* Color of the hovered home button text */ 30 | 31 | --MENU-HEADER-BG-color: #7dc903; /* Background color of menu header */ 32 | --MENU-HEADER-BORDER-color: #7dc903; /*Color of menu header border */ 33 | 34 | --MENU-SEARCH-color: #e0e0e0; /* Color of search field text */ 35 | --MENU-SEARCH-BG-color: #323232; /* Search field background color (by default borders + icons) */ 36 | --MENU-SEARCH-BORDER-color: #e0e0e0; /* Override search field border color */ 37 | 38 | --MENU-SECTIONS-ACTIVE-BG-color: #323232; /* Background color of the active section and its children */ 39 | --MENU-SECTIONS-BG-color: #2b2b2b; /* Background color of other sections */ 40 | --MENU-SECTIONS-LINK-color: #bababa; /* Color of links in menu */ 41 | --MENU-SECTIONS-LINK-HOVER-color: #ffffff; /* Color of links in menu, when hovered */ 42 | --MENU-SECTION-ACTIVE-CATEGORY-color: #82e550; /* Color of active category text */ 43 | --MENU-SECTION-ACTIVE-CATEGORY-BG-color: #202020; /* Color of background for the active category (only) */ 44 | 45 | --MENU-VISITED-color: #569cd8; /* Color of 'page visited' icons in menu */ 46 | --MENU-SECTION-HR-color: #606060; /* Color of
separator in menu */ 47 | 48 | /* base styling for boxes */ 49 | --BOX-CAPTION-color: rgba( 240, 240, 240, 1 ); /* color of the title text */ 50 | --BOX-BG-color: rgba( 20, 20, 20, 1 ); /* color of the content background */ 51 | --BOX-TEXT-color: #e0e0e0; /* automatic color of the content text */ 52 | } 53 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/css/theme-relearn-light.css: -------------------------------------------------------------------------------- 1 | /* here in this showcase we use our own modified chroma syntax highlightning style; 2 | if you want to use a predefined style instead: 3 | - remove `markup.highlight.noClasses` from your config.toml 4 | - set `markup.highlight.style` to a predefined style name in your config.toml 5 | - remove the following `@import` of the self-defined chroma stylesheet */ 6 | @import "chroma-relearn-light.css"; 7 | 8 | :root { 9 | --MAIN-TEXT-color: #101010; /* Color of text by default */ 10 | --MAIN-TITLES-TEXT-color: #4a4a4a; /* Color of titles h2-h3-h4-h5-h6 */ 11 | --MAIN-LINK-color: #486ac9; /* Color of links */ 12 | --MAIN-LINK-HOVER-color: #134fbf; /* Color of hovered links */ 13 | --MAIN-ANCHOR-color: #134fbf; /* color of anchors on titles */ 14 | --MAIN-BG-color: #ffffff; /* color of text by default */ 15 | 16 | /* adjusted to relearn-light chroma style */ 17 | --CODE-BLOCK-color: #000000; /* fallback color for block code text */ 18 | --CODE-BLOCK-BG-color: #f8f8f8; /* fallback color for block code background */ 19 | --CODE-BLOCK-BORDER-color: #d8d8d8; /* color of block code border */ 20 | 21 | --CODE-INLINE-color: #5e5e5e; /* color for inline code text */ 22 | --CODE-INLINE-BG-color: #fffae9; /* color for inline code background */ 23 | --CODE-INLINE-BORDER-color: #f8e8c8; /* color of inline code border */ 24 | 25 | --MENU-HOME-LINK-color: #323232; /* Color of the home button text */ 26 | --MENU-HOME-LINK-HOVER-color: #808080; /* Color of the hovered home button text */ 27 | 28 | --MENU-HEADER-BG-color: #7dc903; /* Background color of menu header */ 29 | --MENU-HEADER-BORDER-color: #7dc903; /*Color of menu header border */ 30 | 31 | --MENU-SEARCH-color: #e0e0e0; /* Color of search field text */ 32 | --MENU-SEARCH-BG-color: #323232; /* Search field background color (by default borders + icons) */ 33 | --MENU-SEARCH-BORDER-color: #e0e0e0; /* Override search field border color */ 34 | 35 | --MENU-SECTIONS-ACTIVE-BG-color: rgba( 0, 0, 0, .166 ); /* Background color of the active section and its children */ 36 | --MENU-SECTIONS-BG-color: #282828; /* Background color of other sections */ 37 | --MENU-SECTIONS-LINK-color: #bababa; /* Color of links in menu */ 38 | --MENU-SECTIONS-LINK-HOVER-color: #ffffff; /* Color of links in menu, when hovered */ 39 | --MENU-SECTION-ACTIVE-CATEGORY-color: #444444; /* Color of active category text */ 40 | --MENU-SECTION-ACTIVE-CATEGORY-BG-color: #ffffff; /* Color of background for the active category (only) */ 41 | 42 | --MENU-VISITED-color: #506397; /* Color of 'page visited' icons in menu */ 43 | --MENU-SECTION-HR-color: #606060; /* Color of
separator in menu */ 44 | 45 | /* base styling for boxes */ 46 | --BOX-CAPTION-color: rgba( 255, 255, 255, 1 ); /* color of the title text */ 47 | --BOX-BG-color: rgba( 255, 255, 255, .833 ); /* color of the content background */ 48 | --BOX-TEXT-color: rgba( 16, 16, 16, 1 ); /* fixed color of the content text */ 49 | } 50 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/css/theme-relearn.css: -------------------------------------------------------------------------------- 1 | /* this file is here for compatiblity with older installations 2 | use theme-relearn-light instead */ 3 | @import "theme-relearn-light.css"; 4 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/fonts/WorkSans-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choria-io/asyncjobs/f1ba2250e3990bcac45d6cf6ba94f74366ad8be4/docs/themes/hugo-theme-relearn/static/fonts/WorkSans-Bold.woff -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/fonts/WorkSans-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choria-io/asyncjobs/f1ba2250e3990bcac45d6cf6ba94f74366ad8be4/docs/themes/hugo-theme-relearn/static/fonts/WorkSans-Bold.woff2 -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/fonts/WorkSans-ExtraLight.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choria-io/asyncjobs/f1ba2250e3990bcac45d6cf6ba94f74366ad8be4/docs/themes/hugo-theme-relearn/static/fonts/WorkSans-ExtraLight.woff -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/fonts/WorkSans-ExtraLight.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choria-io/asyncjobs/f1ba2250e3990bcac45d6cf6ba94f74366ad8be4/docs/themes/hugo-theme-relearn/static/fonts/WorkSans-ExtraLight.woff2 -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/fonts/WorkSans-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choria-io/asyncjobs/f1ba2250e3990bcac45d6cf6ba94f74366ad8be4/docs/themes/hugo-theme-relearn/static/fonts/WorkSans-Light.woff -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/fonts/WorkSans-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choria-io/asyncjobs/f1ba2250e3990bcac45d6cf6ba94f74366ad8be4/docs/themes/hugo-theme-relearn/static/fonts/WorkSans-Light.woff2 -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/fonts/WorkSans-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choria-io/asyncjobs/f1ba2250e3990bcac45d6cf6ba94f74366ad8be4/docs/themes/hugo-theme-relearn/static/fonts/WorkSans-Medium.woff -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/fonts/WorkSans-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choria-io/asyncjobs/f1ba2250e3990bcac45d6cf6ba94f74366ad8be4/docs/themes/hugo-theme-relearn/static/fonts/WorkSans-Medium.woff2 -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/fonts/WorkSans-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choria-io/asyncjobs/f1ba2250e3990bcac45d6cf6ba94f74366ad8be4/docs/themes/hugo-theme-relearn/static/fonts/WorkSans-Regular.woff -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/fonts/WorkSans-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choria-io/asyncjobs/f1ba2250e3990bcac45d6cf6ba94f74366ad8be4/docs/themes/hugo-theme-relearn/static/fonts/WorkSans-Regular.woff2 -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/images/gopher-404.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choria-io/asyncjobs/f1ba2250e3990bcac45d6cf6ba94f74366ad8be4/docs/themes/hugo-theme-relearn/static/images/gopher-404.jpg -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/js/search.js: -------------------------------------------------------------------------------- 1 | var lunrIndex, pagesIndex; 2 | 3 | // Initialize lunrjs using our generated index file 4 | function initLunr() { 5 | // First retrieve the index file 6 | $.getJSON(index_url) 7 | .done(function(index) { 8 | pagesIndex = index; 9 | // Set up lunrjs by declaring the fields we use 10 | // Also provide their boost level for the ranking 11 | lunrIndex = lunr(function() { 12 | this.ref('index'); 13 | this.field('title', { 14 | boost: 15 15 | }); 16 | this.field('tags', { 17 | boost: 10 18 | }); 19 | this.field('content', { 20 | boost: 5 21 | }); 22 | 23 | this.pipeline.remove(lunr.stemmer); 24 | this.searchPipeline.remove(lunr.stemmer); 25 | 26 | // Feed lunr with each file and let lunr actually index them 27 | pagesIndex.forEach(function(page, idx) { 28 | page.index = idx; 29 | this.add(page); 30 | }, this); 31 | }) 32 | }) 33 | .fail(function(jqxhr, textStatus, error) { 34 | var err = textStatus + ', ' + error; 35 | console.error('Error getting Hugo index file:', err); 36 | }); 37 | } 38 | 39 | /** 40 | * Trigger a search in lunr and transform the result 41 | * 42 | * @param {String} term 43 | * @return {Array} results 44 | */ 45 | function search(term) { 46 | // Find the item in our index corresponding to the lunr one to have more info 47 | // Remove Lunr special search characters: https://lunrjs.com/guides/searching.html 48 | var searchTerm = lunr.tokenizer(term.replace(/[*:^~+-]/, ' ')).reduce( function(a,token){return a.concat(searchPatterns(token.str))}, []).join(' '); 49 | return !searchTerm ? [] : lunrIndex.search(searchTerm).map(function(result) { 50 | return { index: result.ref, matches: Object.keys(result.matchData.metadata) } 51 | }); 52 | } 53 | 54 | function searchPatterns(word) { 55 | return [ 56 | word + '^100', 57 | word + '*^10', 58 | '*' + word + '^10', 59 | word + '~' + Math.floor(word.length / 4) + '^1' // allow 1 in 4 letters to have a typo 60 | ]; 61 | } 62 | 63 | // Let's get started 64 | initLunr(); 65 | $(function() { 66 | var searchList = new autoComplete({ 67 | /* selector for the search box element */ 68 | selectorToInsert: '#header-wrapper', 69 | selector: '#search-by', 70 | /* source is the callback to perform the search */ 71 | source: function(term, response) { 72 | response(search(term)); 73 | }, 74 | /* renderItem displays individual search results */ 75 | renderItem: function(item, term) { 76 | var page = pagesIndex[item.index]; 77 | var numContextWords = 2; 78 | var contextPattern = '(?:\\S+ +){0,' + numContextWords + '}\\S*\\b(?:' + 79 | item.matches.map( function(match){return match.replace(/\W/g, '\\$&')} ).join('|') + 80 | ')\\b\\S*(?: +\\S+){0,' + numContextWords + '}'; 81 | var context = page.content.match(new RegExp(contextPattern, 'i')); 82 | var divcontext = document.createElement('div'); 83 | divcontext.className = 'context'; 84 | divcontext.innerText = (context || ''); 85 | var divsuggestion = document.createElement('div'); 86 | divsuggestion.className = 'autocomplete-suggestion'; 87 | divsuggestion.setAttribute('data-term', term); 88 | divsuggestion.setAttribute('data-title', page.title); 89 | divsuggestion.setAttribute('data-uri', baseUri + page.uri); 90 | divsuggestion.setAttribute('data-context', context); 91 | divsuggestion.innerText = '» ' + page.title; 92 | divsuggestion.appendChild(divcontext); 93 | return divsuggestion.outerHTML; 94 | }, 95 | /* onSelect callback fires when a search suggestion is chosen */ 96 | onSelect: function(e, term, item) { 97 | location.href = item.getAttribute('data-uri'); 98 | } 99 | }); 100 | 101 | // JavaScript-autoComplete only registers the focus event when minChars is 0 which doesn't make sense, let's do it ourselves 102 | // https://github.com/Pixabay/JavaScript-autoComplete/blob/master/auto-complete.js#L191 103 | var selector = $('#search-by').get(0); 104 | $(selector).focus(selector.focusHandler); 105 | }); 106 | -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/webfonts/fa-brands-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choria-io/asyncjobs/f1ba2250e3990bcac45d6cf6ba94f74366ad8be4/docs/themes/hugo-theme-relearn/static/webfonts/fa-brands-400.eot -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choria-io/asyncjobs/f1ba2250e3990bcac45d6cf6ba94f74366ad8be4/docs/themes/hugo-theme-relearn/static/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/webfonts/fa-brands-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choria-io/asyncjobs/f1ba2250e3990bcac45d6cf6ba94f74366ad8be4/docs/themes/hugo-theme-relearn/static/webfonts/fa-brands-400.woff -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choria-io/asyncjobs/f1ba2250e3990bcac45d6cf6ba94f74366ad8be4/docs/themes/hugo-theme-relearn/static/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/webfonts/fa-regular-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choria-io/asyncjobs/f1ba2250e3990bcac45d6cf6ba94f74366ad8be4/docs/themes/hugo-theme-relearn/static/webfonts/fa-regular-400.eot -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choria-io/asyncjobs/f1ba2250e3990bcac45d6cf6ba94f74366ad8be4/docs/themes/hugo-theme-relearn/static/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/webfonts/fa-regular-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choria-io/asyncjobs/f1ba2250e3990bcac45d6cf6ba94f74366ad8be4/docs/themes/hugo-theme-relearn/static/webfonts/fa-regular-400.woff -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choria-io/asyncjobs/f1ba2250e3990bcac45d6cf6ba94f74366ad8be4/docs/themes/hugo-theme-relearn/static/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/webfonts/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choria-io/asyncjobs/f1ba2250e3990bcac45d6cf6ba94f74366ad8be4/docs/themes/hugo-theme-relearn/static/webfonts/fa-solid-900.eot -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choria-io/asyncjobs/f1ba2250e3990bcac45d6cf6ba94f74366ad8be4/docs/themes/hugo-theme-relearn/static/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/webfonts/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choria-io/asyncjobs/f1ba2250e3990bcac45d6cf6ba94f74366ad8be4/docs/themes/hugo-theme-relearn/static/webfonts/fa-solid-900.woff -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/static/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choria-io/asyncjobs/f1ba2250e3990bcac45d6cf6ba94f74366ad8be4/docs/themes/hugo-theme-relearn/static/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /docs/themes/hugo-theme-relearn/theme.toml: -------------------------------------------------------------------------------- 1 | name = "Relearn" 2 | license = "MIT" 3 | licenselink = "https://github.com/McShelby/hugo-theme-relearn/blob/main/LICENSE" 4 | description = "A theme for Hugo designed for documentation" 5 | homepage = "https://github.com/McShelby/hugo-theme-relearn" 6 | demosite = "https://mcshelby.github.io/hugo-theme-relearn" 7 | tags = ["dark", "dark mode", "docs", "light", "multilingual", "responsive"] 8 | features = ["dark mode", "documentation", "expand", "include", "light mode", "menu", "mermaid", "multilingual", "nested sections", "notice", "oas", "search", "swagger", "tabs", "themeable"] 9 | 10 | [module] 11 | [module.hugoVersion] 12 | min = "0.93.0" 13 | 14 | [author] 15 | name = "Sören Weber" 16 | homepage = "https://github.com/McShelby" 17 | 18 | [original] 19 | author = "Mathieu Cornic" 20 | homepage = "https://learn.netlify.app" 21 | repo = "https://github.com/matcornic/hugo-theme-learn" 22 | -------------------------------------------------------------------------------- /election/options.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021, R.I. Pienaar and the Project contributors 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package election 6 | 7 | import ( 8 | "time" 9 | 10 | "github.com/nats-io/nats.go" 11 | ) 12 | 13 | // Option configures the election system 14 | type Option func(o *options) 15 | 16 | type options struct { 17 | name string 18 | key string 19 | bucket nats.KeyValue 20 | ttl time.Duration 21 | cInterval time.Duration 22 | wonCb func() 23 | lostCb func() 24 | campaignCb func(s State) 25 | bo Backoff 26 | debug func(format string, a ...any) 27 | } 28 | 29 | // WithBackoff will use the provided Backoff timer source to decrease campaign intervals over time 30 | func WithBackoff(bo Backoff) Option { 31 | return func(o *options) { o.bo = bo } 32 | } 33 | 34 | // OnWon is a callback called when winning an election 35 | func OnWon(cb func()) Option { 36 | return func(o *options) { o.wonCb = cb } 37 | } 38 | 39 | // OnLost is a callback called when losing an election 40 | func OnLost(cb func()) Option { 41 | return func(o *options) { o.lostCb = cb } 42 | } 43 | 44 | // OnCampaign is called each time a campaign is done by the leader or a candidate 45 | func OnCampaign(cb func(s State)) Option { 46 | return func(o *options) { o.campaignCb = cb } 47 | } 48 | 49 | // WithDebug sets a function to do debug logging with 50 | func WithDebug(cb func(format string, a ...any)) Option { 51 | return func(o *options) { o.debug = cb } 52 | } 53 | -------------------------------------------------------------------------------- /election/stats.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2021, R.I. Pienaar and the Project contributors 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package election 6 | 7 | import ( 8 | "github.com/prometheus/client_golang/prometheus" 9 | ) 10 | 11 | var ( 12 | prometheusNamespace = "choria_asyncjobs" 13 | 14 | campaignsCounter = prometheus.NewCounterVec(prometheus.CounterOpts{ 15 | Name: prometheus.BuildFQName(prometheusNamespace, "election", "campaigns"), 16 | Help: "The number of campaigns a specific candidate voted in", 17 | }, []string{"election", "identity", "state"}) 18 | 19 | leaderGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 20 | Name: prometheus.BuildFQName(prometheusNamespace, "election", "leader"), 21 | Help: "Indicates if a specific instance is the current leader", 22 | }, []string{"election", "identity"}) 23 | 24 | campaignIntervalGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 25 | Name: prometheus.BuildFQName(prometheusNamespace, "election", "interval_seconds"), 26 | Help: "The number of seconds between campaigns", 27 | }, []string{"election", "identity"}) 28 | ) 29 | 30 | func init() { 31 | prometheus.MustRegister(campaignsCounter) 32 | prometheus.MustRegister(leaderGauge) 33 | prometheus.MustRegister(campaignIntervalGauge) 34 | } 35 | -------------------------------------------------------------------------------- /generators/fs/godocker/Dockerfile.templ: -------------------------------------------------------------------------------- 1 | FROM golang:latest AS builder 2 | 3 | WORKDIR /usr/src/app 4 | 5 | RUN go mod init "{{ .Package.Name }}" && \ 6 | {{- range $handler := .Package.TaskHandlers }} 7 | {{- if $handler.Package }} 8 | go get "{{ $handler.Package }}@{{ $handler.Version }}" && \ 9 | {{- end }} 10 | {{- end }} 11 | go get github.com/choria-io/asyncjobs@{{ .Package.AJVersion }} 12 | 13 | COPY main.go /usr/src/app/main.go 14 | 15 | RUN go mod tidy 16 | RUN go build -v -o /app -ldflags="-s -w -extldflags=-static" 17 | 18 | FROM alpine:latest 19 | 20 | RUN addgroup -g 2048 asyncjobs && \ 21 | adduser -u 2048 -h /home/asyncjobs -g "Choria Asynchronous Jobs" -S -D -H -G asyncjobs asyncjobs && \ 22 | mkdir /lib64 && \ 23 | ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2 && \ 24 | apk --no-cache add ca-certificates && \ 25 | mkdir -p /handler/config 26 | 27 | COPY --from=builder /app /handler/app 28 | {{- range $handler := .Package.TaskHandlers }} 29 | {{- if $handler.Command }} 30 | COPY ./commands/{{ $handler.Command }} /handler/commands/{{ $handler.Command }} 31 | {{- end }} 32 | {{- end }} 33 | 34 | EXPOSE 8080/tcp 35 | 36 | USER asyncjobs 37 | 38 | ENV XDG_CONFIG_HOME "/handler/config" 39 | ENV AJ_WORK_QUEUE "{{ .Package.WorkQueue }}" 40 | {{- if .Package.ContextName }} 41 | ENV AJ_NATS_CONTEXT "{{ .Package.ContextName }}" 42 | {{- else }} 43 | ENV AJ_NATS_CONTEXT "AJ" 44 | {{- end }} 45 | 46 | WORKDIR "/handler" 47 | ENTRYPOINT ["/handler/app"] 48 | -------------------------------------------------------------------------------- /generators/godocker.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, R.I. Pienaar and the Project contributors 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package generators 6 | 7 | import ( 8 | "bytes" 9 | "embed" 10 | "fmt" 11 | "os" 12 | "path/filepath" 13 | "strings" 14 | "text/template" 15 | "time" 16 | 17 | aj "github.com/choria-io/asyncjobs" 18 | ) 19 | 20 | // GoContainer builds docker containers based on the package spec 21 | type GoContainer struct { 22 | // Package describes the package to build 23 | Package *Package 24 | // BuildTime is when the package is being built, set at runtime 25 | BuildTime string 26 | } 27 | 28 | var ( 29 | //go:embed fs/godocker 30 | godockerFS embed.FS 31 | ) 32 | 33 | // NewGoContainer create a new go container builder 34 | func NewGoContainer(handlers *Package) (*GoContainer, error) { 35 | return &GoContainer{ 36 | Package: handlers, 37 | BuildTime: time.Now().Format(time.RFC822), 38 | }, nil 39 | } 40 | 41 | // RenderToDirectory renders the container to a specific directory 42 | func (g *GoContainer) RenderToDirectory(target string) error { 43 | files, err := godockerFS.ReadDir("fs/godocker") 44 | if err != nil { 45 | return err 46 | } 47 | 48 | if g.Package.RetryPolicy == "" { 49 | g.Package.RetryPolicy = "default" 50 | } 51 | 52 | for _, p := range g.Package.TaskHandlers { 53 | if p.RequestReply || p.Command != "" { 54 | continue 55 | } 56 | 57 | if p.Package == "" { 58 | return fmt.Errorf("task handlers require a package") 59 | } 60 | 61 | if p.Version == "" { 62 | return fmt.Errorf("task handlers require a version") 63 | } 64 | } 65 | 66 | funcs := map[string]any{ 67 | "RetryNamesList": func() string { 68 | return strings.Join(aj.RetryPolicyNames(), ", ") 69 | }, 70 | "TypeToPackageName": func(t string) string { 71 | remove := []string{"_", "-", ":", "/", "\\"} 72 | res := t 73 | 74 | for _, r := range remove { 75 | res = strings.Replace(res, r, "", -1) 76 | } 77 | 78 | return res 79 | }, 80 | } 81 | 82 | for _, f := range files { 83 | if f.IsDir() { 84 | continue 85 | } 86 | 87 | t := template.New(f.Name()) 88 | t.Funcs(funcs) 89 | body, err := godockerFS.ReadFile(filepath.Join("fs/godocker", f.Name())) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | p, err := t.Parse(string(body)) 95 | if err != nil { 96 | return err 97 | } 98 | 99 | buf := bytes.NewBuffer([]byte{}) 100 | 101 | err = p.Execute(buf, g) 102 | if err != nil { 103 | return err 104 | } 105 | 106 | err = os.WriteFile(filepath.Join(target, strings.TrimSuffix(filepath.Base(f.Name()), ".templ")), buf.Bytes(), 0644) 107 | if err != nil { 108 | return err 109 | } 110 | } 111 | 112 | return nil 113 | } 114 | -------------------------------------------------------------------------------- /generators/package.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, R.I. Pienaar and the Project contributors 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package generators 6 | 7 | // Generator is the interfaces generators must implement 8 | type Generator interface { 9 | // RenderToDirectory renders the output to directory target 10 | RenderToDirectory(target string) error 11 | } 12 | 13 | // Package describe a configuration of a asyncjobs handler with multiple handlers loaded 14 | type Package struct { 15 | // ContextName is the optional NATS Context name to use when none is configured 16 | ContextName string `yaml:"nats"` 17 | // WorkQueue is the optional Work Queue name to bind to, else DEFAULT will be used 18 | WorkQueue string `yaml:"queue"` 19 | // TaskHandlers is a list of handlers for tasks 20 | TaskHandlers []TaskHandler `yaml:"tasks"` 21 | // Name is an optional name for the generated go package 22 | Name string `yaml:"name"` 23 | // AJVersion is an optional version to use for the choria-io/asyncjobs dependency 24 | AJVersion string `yaml:"asyncjobs"` 25 | // RetryPolicy is the name of a retry policy, see RetryPolicyNames() 26 | RetryPolicy string `yaml:"retry"` 27 | // DiscardStates indicates what termination states to discard 28 | DiscardStates []string `yaml:"discard"` 29 | // TaskSignaturesOptional allows unsigned tasks to be used when AJ_VERIFICATION_KEY is set 30 | TaskSignaturesOptional bool `yaml:"task_signatures_optional"` 31 | } 32 | 33 | // TaskHandler is an individual Task Handler 34 | type TaskHandler struct { 35 | // TaskType is the type to handle like email:new 36 | TaskType string `yaml:"type"` 37 | // Package is a golang package name that has a AsyncJobHandler() implementing HandlerFunc 38 | Package string `yaml:"package"` 39 | // Version is the version to fetch of this package 40 | Version string `yaml:"version"` 41 | // RequestReply indicates the handler is a callout to a remote service 42 | RequestReply bool `yaml:"remote"` 43 | // Command indicates the handler is a callout to a command in the given file 44 | Command string `yaml:"command"` 45 | } 46 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/choria-io/asyncjobs 2 | 3 | go 1.23.6 4 | 5 | require ( 6 | github.com/AlecAivazis/survey/v2 v2.3.7 7 | github.com/choria-io/fisk v0.7.1 8 | github.com/dustin/go-humanize v1.0.1 9 | github.com/nats-io/jsm.go v0.2.3 10 | github.com/nats-io/nats-server/v2 v2.11.4 11 | github.com/nats-io/nats.go v1.42.0 12 | github.com/onsi/ginkgo/v2 v2.23.4 13 | github.com/onsi/gomega v1.37.0 14 | github.com/prometheus/client_golang v1.22.0 15 | github.com/robfig/cron/v3 v3.0.1 16 | github.com/segmentio/ksuid v1.0.4 17 | github.com/sirupsen/logrus v1.9.3 18 | github.com/xlab/tablewriter v0.0.0-20160610135559-80b567a11ad5 19 | golang.org/x/term v0.32.0 20 | gopkg.in/yaml.v3 v3.0.1 21 | ) 22 | 23 | require ( 24 | github.com/beorn7/perks v1.0.1 // indirect 25 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 26 | github.com/expr-lang/expr v1.17.3 // indirect 27 | github.com/go-logr/logr v1.4.2 // indirect 28 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 29 | github.com/google/go-cmp v0.7.0 // indirect 30 | github.com/google/go-tpm v0.9.5 // indirect 31 | github.com/google/pprof v0.0.0-20250501235452-c0086092b71a // indirect 32 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect 33 | github.com/klauspost/compress v1.18.0 // indirect 34 | github.com/mattn/go-colorable v0.1.14 // indirect 35 | github.com/mattn/go-isatty v0.0.20 // indirect 36 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect 37 | github.com/minio/highwayhash v1.0.3 // indirect 38 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 39 | github.com/nats-io/jwt/v2 v2.7.4 // indirect 40 | github.com/nats-io/nkeys v0.4.11 // indirect 41 | github.com/nats-io/nuid v1.0.1 // indirect 42 | github.com/prometheus/client_model v0.6.2 // indirect 43 | github.com/prometheus/common v0.64.0 // indirect 44 | github.com/prometheus/procfs v0.16.1 // indirect 45 | go.uber.org/automaxprocs v1.6.0 // indirect 46 | golang.org/x/crypto v0.38.0 // indirect 47 | golang.org/x/net v0.40.0 // indirect 48 | golang.org/x/sys v0.33.0 // indirect 49 | golang.org/x/text v0.25.0 // indirect 50 | golang.org/x/time v0.11.0 // indirect 51 | golang.org/x/tools v0.33.0 // indirect 52 | google.golang.org/protobuf v1.36.6 // indirect 53 | ) 54 | -------------------------------------------------------------------------------- /lifecycle.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, R.I. Pienaar and the Project contributors 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package asyncjobs 6 | 7 | import ( 8 | "encoding/json" 9 | "fmt" 10 | "time" 11 | 12 | "github.com/segmentio/ksuid" 13 | ) 14 | 15 | // BaseEvent is present in all event types and can be used to detect the type 16 | type BaseEvent struct { 17 | EventID string `json:"event_id"` 18 | EventType string `json:"type"` 19 | TimeStamp time.Time `json:"timestamp"` 20 | } 21 | 22 | // TaskStateChangeEvent notifies that a significant change occurred in a Task 23 | type TaskStateChangeEvent struct { 24 | BaseEvent 25 | 26 | // TaskID is the ID of the task, use with LoadTaskByID() to access the task 27 | TaskID string `json:"task_id"` 28 | // State is the new state of the Task 29 | State TaskState `json:"state"` 30 | // Tries is how many times the Task has been processed 31 | Tries int `json:"tries"` 32 | // Queue is the queue the task is in, can be empty 33 | Queue string `json:"queue,omitempty"` 34 | // TaskType is the task routing type 35 | TaskType string `json:"task_type"` 36 | // LstErr is the error that caused a task to change state for error state changes 37 | LastErr string `json:"last_error,omitempty"` 38 | // Age is the time since the task was created in milliseconds 39 | Age time.Duration `json:"task_age,omitempty"` 40 | } 41 | 42 | // LeaderElectedEvent notifies that a leader election was won 43 | type LeaderElectedEvent struct { 44 | BaseEvent 45 | 46 | // Name of the process that gained leadership 47 | Name string `json:"name"` 48 | // Component is the component that is reporting 49 | Component string `json:"component"` 50 | } 51 | 52 | const ( 53 | // TaskStateChangeEventType is the event type for TaskStateChangeEvent events 54 | TaskStateChangeEventType = "io.choria.asyncjobs.v1.task_state" 55 | 56 | // LeaderElectedEventType is the event type for LeaderElectedEvent events 57 | LeaderElectedEventType = "io.choria.asyncjobs.v1.leader_elected" 58 | ) 59 | 60 | // ParseEventJSON parses event bytes returning the parsed Event and its event type 61 | func ParseEventJSON(event []byte) (any, string, error) { 62 | var base BaseEvent 63 | err := json.Unmarshal(event, &base) 64 | if err != nil { 65 | return nil, "", err 66 | } 67 | 68 | switch base.EventType { 69 | case TaskStateChangeEventType: 70 | var e TaskStateChangeEvent 71 | err := json.Unmarshal(event, &e) 72 | if err != nil { 73 | return nil, "", err 74 | } 75 | 76 | return e, base.EventType, nil 77 | 78 | case LeaderElectedEventType: 79 | var e LeaderElectedEvent 80 | err := json.Unmarshal(event, &e) 81 | if err != nil { 82 | return nil, "", err 83 | } 84 | 85 | return e, base.EventType, nil 86 | default: 87 | return nil, base.EventType, fmt.Errorf("%w: %s", ErrUnknownEventType, base.EventType) 88 | } 89 | } 90 | 91 | // NewLeaderElectedEvent creates a new event notifying of a leader election win 92 | func NewLeaderElectedEvent(name string, component string) (*LeaderElectedEvent, error) { 93 | eid, err := ksuid.NewRandom() 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | return &LeaderElectedEvent{ 99 | Name: name, 100 | Component: component, 101 | BaseEvent: BaseEvent{ 102 | EventID: eid.String(), 103 | TimeStamp: eid.Time().UTC(), 104 | EventType: LeaderElectedEventType, 105 | }, 106 | }, nil 107 | } 108 | 109 | // NewTaskStateChangeEvent creates a new event notifying of a change in task state 110 | func NewTaskStateChangeEvent(t *Task) (*TaskStateChangeEvent, error) { 111 | eid, err := ksuid.NewRandom() 112 | if err != nil { 113 | return nil, err 114 | } 115 | 116 | e := &TaskStateChangeEvent{ 117 | TaskID: t.ID, 118 | State: t.State, 119 | Tries: t.Tries, 120 | Queue: t.Queue, 121 | TaskType: t.Type, 122 | LastErr: t.LastErr, 123 | BaseEvent: BaseEvent{ 124 | EventID: eid.String(), 125 | TimeStamp: eid.Time().UTC(), 126 | EventType: TaskStateChangeEventType, 127 | }, 128 | } 129 | 130 | if !t.CreatedAt.IsZero() { 131 | e.Age = time.Since(t.CreatedAt.Round(time.Millisecond)) 132 | } 133 | 134 | return e, nil 135 | } 136 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, R.I. Pienaar and the Project contributors 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package asyncjobs 6 | 7 | import ( 8 | "log" 9 | ) 10 | 11 | // Logger is a pluggable logger interface 12 | type Logger interface { 13 | Debugf(format string, v ...any) 14 | Infof(format string, v ...any) 15 | Warnf(format string, v ...any) 16 | Errorf(format string, v ...any) 17 | } 18 | 19 | // Default console logger 20 | type defaultLogger struct{} 21 | 22 | func (l *defaultLogger) Infof(format string, v ...any) { 23 | log.Printf(format, v...) 24 | } 25 | 26 | func (l *defaultLogger) Warnf(format string, v ...any) { 27 | log.Printf(format, v...) 28 | } 29 | 30 | func (l *defaultLogger) Errorf(format string, v ...any) { 31 | log.Printf(format, v...) 32 | } 33 | 34 | func (l *defaultLogger) Debugf(format string, v ...any) { 35 | log.Printf(format, v...) 36 | } 37 | 38 | // Logger placeholder 39 | type noopLogger struct{} 40 | 41 | func (l *noopLogger) Infof(format string, v ...any) {} 42 | func (l *noopLogger) Warnf(format string, v ...any) {} 43 | func (l *noopLogger) Errorf(format string, v ...any) {} 44 | func (l *noopLogger) Debugf(format string, v ...any) {} 45 | -------------------------------------------------------------------------------- /mux.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, R.I. Pienaar and the Project contributors 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package asyncjobs 6 | 7 | import ( 8 | "context" 9 | "encoding/json" 10 | "fmt" 11 | "os" 12 | "os/exec" 13 | "sort" 14 | "strings" 15 | "sync" 16 | "time" 17 | 18 | "github.com/dustin/go-humanize" 19 | ) 20 | 21 | type entryHandler struct { 22 | ttype string 23 | hf HandlerFunc 24 | } 25 | 26 | // HandlerFunc handles a single task, the response bytes will be stored in the original task 27 | type HandlerFunc func(ctx context.Context, log Logger, t *Task) (any, error) 28 | 29 | // Mux routes messages 30 | // 31 | // Note: this will change to be nearer to a server mux and include support for middleware 32 | type Mux struct { 33 | hf map[string]*entryHandler 34 | ehf []*entryHandler 35 | mu *sync.Mutex 36 | } 37 | 38 | // NewTaskRouter creates a new Mux 39 | func NewTaskRouter() *Mux { 40 | return &Mux{ 41 | hf: map[string]*entryHandler{}, 42 | ehf: []*entryHandler{}, 43 | mu: &sync.Mutex{}, 44 | } 45 | } 46 | 47 | func notFoundHandler(_ context.Context, _ Logger, t *Task) (any, error) { 48 | return nil, fmt.Errorf("%w %q", ErrNoHandlerForTaskType, t.Type) 49 | } 50 | 51 | // Handler looks up the handler function for a task 52 | func (m *Mux) Handler(t *Task) HandlerFunc { 53 | m.mu.Lock() 54 | defer m.mu.Unlock() 55 | 56 | hf, ok := m.hf[t.Type] 57 | if ok { 58 | return hf.hf 59 | } 60 | 61 | for _, hf := range m.ehf { 62 | if strings.HasPrefix(t.Type, hf.ttype) { 63 | return hf.hf 64 | } 65 | } 66 | 67 | return notFoundHandler 68 | } 69 | 70 | // HandleFunc registers a task for a taskType. The taskType must match exactly with the matching tasks 71 | func (m *Mux) HandleFunc(taskType string, h HandlerFunc) error { 72 | m.mu.Lock() 73 | defer m.mu.Unlock() 74 | 75 | _, ok := m.hf[taskType] 76 | if ok { 77 | return fmt.Errorf("%w %q", ErrDuplicateHandlerForTaskType, taskType) 78 | } 79 | 80 | m.hf[taskType] = &entryHandler{hf: h, ttype: taskType} 81 | m.ehf = append(m.ehf, m.hf[taskType]) 82 | 83 | sort.Slice(m.ehf, func(i, j int) bool { 84 | return len(m.ehf[i].ttype) > len(m.ehf[j].ttype) 85 | }) 86 | 87 | return nil 88 | } 89 | 90 | // RequestReply sets up a delegated handler via NATS Request-Reply 91 | func (m *Mux) RequestReply(taskType string, client *Client) error { 92 | h := newRequestReplyHandleFunc(client.opts.nc, taskType) 93 | return m.HandleFunc(taskType, h) 94 | } 95 | 96 | // ExternalProcess sets up a delegated handler that calls an external command to handle the task. 97 | // 98 | // The task will be passed in JSON format on STDIN, any STDOUT/STDERR output will become the task 99 | // result. Any non 0 exit code will be treated as a task failure. 100 | func (m *Mux) ExternalProcess(taskType string, command string) error { 101 | return m.HandleFunc(taskType, func(ctx context.Context, log Logger, task *Task) (any, error) { 102 | stat, err := os.Stat(command) 103 | if err != nil || stat.IsDir() { 104 | return nil, ErrExternalCommandNotFound 105 | } 106 | 107 | tj, err := json.Marshal(task) 108 | if err != nil { 109 | return nil, err 110 | } 111 | 112 | stdinFile, err := os.CreateTemp("", "asyncjobs-task") 113 | if err != nil { 114 | return nil, err 115 | } 116 | defer os.Remove(stdinFile.Name()) 117 | defer stdinFile.Close() 118 | 119 | _, err = stdinFile.Write(tj) 120 | if err != nil { 121 | return nil, err 122 | } 123 | stdinFile.Close() 124 | 125 | start := time.Now() 126 | log.Infof("Running task %s try %d using %q", task.ID, task.Tries, command) 127 | 128 | cmd := exec.CommandContext(ctx, command) 129 | cmd.Env = append(cmd.Env, fmt.Sprintf("CHORIA_AJ_TASK=%s", stdinFile.Name())) 130 | out, err := cmd.CombinedOutput() 131 | if err != nil { 132 | log.Errorf("Running %s failed: %q", command, out) 133 | return nil, fmt.Errorf("%w: %v", ErrExternalCommandFailed, err) 134 | } 135 | 136 | log.Infof("Task %s completed using %q after %s and %d tries with %s payload", task.ID, command, time.Since(start), task.Tries, humanize.IBytes(uint64(len(out)))) 137 | 138 | return string(out), nil 139 | }) 140 | } 141 | -------------------------------------------------------------------------------- /mux_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, R.I. Pienaar and the Project contributors 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package asyncjobs 6 | 7 | import ( 8 | "context" 9 | 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | ) 13 | 14 | var _ = Describe("Router", func() { 15 | Describe("ExternalProcess", func() { 16 | var ( 17 | task *Task 18 | err error 19 | router *Mux 20 | ) 21 | BeforeEach(func() { 22 | task, err = NewTask("email:new", nil) 23 | Expect(err).ToNot(HaveOccurred()) 24 | router = NewTaskRouter() 25 | }) 26 | 27 | It("Should handle missing commands", func() { 28 | Expect(router.ExternalProcess("email:new", "testdata/missing.sh")).ToNot(HaveOccurred()) 29 | handler := router.Handler(task) 30 | _, err = handler(context.Background(), &defaultLogger{}, task) 31 | Expect(err).To(MatchError(ErrExternalCommandNotFound)) 32 | }) 33 | 34 | It("Should handle command failures", func() { 35 | Expect(router.ExternalProcess("email:new", "testdata/failing-handler.sh")).ToNot(HaveOccurred()) 36 | handler := router.Handler(task) 37 | _, err = handler(context.Background(), &defaultLogger{}, task) 38 | Expect(err).To(MatchError(ErrExternalCommandFailed)) 39 | }) 40 | 41 | It("Should handle success", func() { 42 | Expect(router.ExternalProcess("email:new", "testdata/passing-handler.sh")).ToNot(HaveOccurred()) 43 | handler := router.Handler(task) 44 | payload, err := handler(context.Background(), &defaultLogger{}, task) 45 | Expect(err).ToNot(HaveOccurred()) 46 | Expect(payload).To(Equal("success\n")) 47 | }) 48 | }) 49 | 50 | Describe("Handler", func() { 51 | It("Should support default handler", func() { 52 | router := NewTaskRouter() 53 | router.HandleFunc("x", func(_ context.Context, _ Logger, _ *Task) (any, error) { 54 | return "x", nil 55 | }) 56 | 57 | task, err := NewTask("y", nil) 58 | Expect(err).ToNot(HaveOccurred()) 59 | 60 | handler := router.Handler(task) 61 | _, err = handler(nil, &defaultLogger{}, task) 62 | Expect(err).To(MatchError(ErrNoHandlerForTaskType)) 63 | }) 64 | 65 | It("Should find the correct handler", func() { 66 | router := NewTaskRouter() 67 | router.HandleFunc("", func(_ context.Context, _ Logger, _ *Task) (any, error) { 68 | return "custom default", nil 69 | }) 70 | router.HandleFunc("things:", func(_ context.Context, _ Logger, _ *Task) (any, error) { 71 | return "things:", nil 72 | }) 73 | router.HandleFunc("things:very:specific", func(_ context.Context, _ Logger, _ *Task) (any, error) { 74 | return "things:very:specific", nil 75 | }) 76 | router.HandleFunc("things:specific", func(_ context.Context, _ Logger, _ *Task) (any, error) { 77 | return "things:specific", nil 78 | }) 79 | 80 | check := func(ttype string, expected string) { 81 | task := &Task{Type: ttype} 82 | res, err := router.Handler(task)(context.Background(), &defaultLogger{}, task) 83 | Expect(err).ToNot(HaveOccurred()) 84 | Expect(res).To(Equal(expected)) 85 | } 86 | 87 | check("things:other", "things:") 88 | check("things:very:other", "things:") 89 | check("things:very:specific", "things:very:specific") 90 | check("things:specific", "things:specific") 91 | check("things:specific:other", "things:specific") 92 | check("x", "custom default") 93 | }) 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /queue.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, R.I. Pienaar and the Project contributors 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package asyncjobs 6 | 7 | import ( 8 | "context" 9 | "sync" 10 | "time" 11 | 12 | "github.com/nats-io/jsm.go/api" 13 | ) 14 | 15 | // Queue represents a work queue 16 | type Queue struct { 17 | // Name is a unique name for the work queue, should be in the character range a-zA-Z0-9 18 | Name string `json:"name"` 19 | // MaxAge is the absolute longest time an entry can stay in the queue. When not set items will not expire 20 | MaxAge time.Duration `json:"max_age"` 21 | // MaxEntries represents the maximum amount of entries that can be in the queue. When it's full new entries will be rejected. When unset no limit is applied. 22 | MaxEntries int `json:"max_entries"` 23 | // DiscardOld indicates that when MaxEntries are reached old entries will be discarded rather than new ones rejected 24 | DiscardOld bool `json:"discard_old"` 25 | // MaxTries is the maximum amount of times a entry can be tried, entries will be tried every MaxRunTime with some jitter applied. Default to DefaultMaxTries 26 | MaxTries int `json:"max_tries"` 27 | // MaxRunTime is the maximum time a task can be processed. Defaults to DefaultJobRunTime 28 | MaxRunTime time.Duration `json:"max_runtime"` 29 | // MaxConcurrent is the total number of in-flight tasks across all active task handlers combined. Defaults to DefaultQueueMaxConcurrent 30 | MaxConcurrent int `json:"max_concurrent"` 31 | // NoCreate will not try to create a queue, will bind to an existing one or fail 32 | NoCreate bool 33 | 34 | mu sync.Mutex 35 | storage Storage 36 | } 37 | 38 | // QueueInfo holds information about a queue state 39 | type QueueInfo struct { 40 | // Name is the name of the queue 41 | Name string `json:"name"` 42 | // Time is the information was gathered 43 | Time time.Time `json:"time"` 44 | // Stream is the active JetStream Stream Information 45 | Stream *api.StreamInfo `json:"stream_info"` 46 | // Consumer is the worker stream information 47 | Consumer *api.ConsumerInfo `json:"consumer_info"` 48 | } 49 | 50 | func (q *Queue) retryTaskByID(ctx context.Context, id string) error { 51 | return q.storage.RetryTaskByID(ctx, q, id) 52 | } 53 | 54 | func (q *Queue) enqueueTask(ctx context.Context, task *Task) error { 55 | task.Queue = q.Name 56 | return q.storage.EnqueueTask(ctx, q, task) 57 | } 58 | 59 | func newDefaultQueue() *Queue { 60 | return &Queue{ 61 | Name: "DEFAULT", 62 | MaxRunTime: time.Minute, 63 | MaxTries: 100, 64 | MaxConcurrent: DefaultQueueMaxConcurrent, 65 | MaxAge: 0, 66 | DiscardOld: false, 67 | mu: sync.Mutex{}, 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /request_reply_handler.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, R.I. Pienaar and the Project contributors 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package asyncjobs 6 | 7 | import ( 8 | "context" 9 | "encoding/json" 10 | "errors" 11 | "fmt" 12 | "time" 13 | 14 | "github.com/nats-io/nats.go" 15 | ) 16 | 17 | const ( 18 | // RequestReplyContentTypeHeader is the header text sent to indicate the body encoding and type 19 | RequestReplyContentTypeHeader = "AJ-Content-Type" 20 | // RequestReplyDeadlineHeader is the header indicating the deadline for processing the item 21 | RequestReplyDeadlineHeader = "AJ-Handler-Deadline" 22 | // RequestReplyTerminateError is the header to send in a reply that the task should be terminated via ErrTerminateTask 23 | RequestReplyTerminateError = "AJ-Terminate" 24 | // RequestReplyError is the header indicating a generic failure in handling an item 25 | RequestReplyError = "AJ-Error" 26 | // RequestReplyTaskType is the content type indicating the payload is a Task in JSON format 27 | RequestReplyTaskType = "application/x-asyncjobs-task+json" 28 | ) 29 | 30 | type requestReplyHandler struct { 31 | nc *nats.Conn 32 | tt string 33 | subj string 34 | } 35 | 36 | func newRequestReplyHandleFunc(nc *nats.Conn, tt string) HandlerFunc { 37 | h := &requestReplyHandler{ 38 | nc: nc, 39 | tt: tt, 40 | } 41 | 42 | h.subj = RequestReplySubjectForTaskType(tt) 43 | 44 | return h.processTask 45 | } 46 | 47 | // RequestReplySubjectForTaskType returns the subject a request-reply handler should listen on for a specified task type 48 | func RequestReplySubjectForTaskType(taskType string) string { 49 | if taskType == "" { 50 | return fmt.Sprintf(RequestReplyTaskHandlerPattern, "catchall") 51 | } 52 | return fmt.Sprintf(RequestReplyTaskHandlerPattern, taskType) 53 | } 54 | 55 | func (r *requestReplyHandler) processTask(ctx context.Context, logger Logger, task *Task) (any, error) { 56 | if r.nc == nil { 57 | return nil, fmt.Errorf("no connnection set") 58 | } 59 | 60 | var err error 61 | 62 | deadline, ok := ctx.Deadline() 63 | if !ok { 64 | return nil, ErrRequestReplyNoDeadline 65 | } 66 | if time.Until(deadline) < 3*time.Second { 67 | return nil, ErrRequestReplyShortDeadline 68 | } 69 | 70 | msg := nats.NewMsg(r.subj) 71 | msg.Header.Add(RequestReplyContentTypeHeader, RequestReplyTaskType) 72 | msg.Header.Add(RequestReplyDeadlineHeader, deadline.Add(-2*time.Second).UTC().Format(time.RFC3339)) 73 | msg.Data, err = json.Marshal(task) 74 | if err != nil { 75 | return nil, fmt.Errorf("could not encode task: %v", err) 76 | } 77 | 78 | logger.Infof("Calling request-reply handler on %s", msg.Subject) 79 | res, err := r.nc.RequestMsgWithContext(ctx, msg) 80 | switch { 81 | case err == context.DeadlineExceeded: 82 | logger.Errorf("Request-Reply callout failed, no response received within %v", deadline) 83 | return nil, fmt.Errorf("%w: %v", ErrRequestReplyFailed, err) 84 | case err == nats.ErrNoResponders: 85 | logger.Errorf("Request-Reply handler failed, no responders on subject %s", msg.Subject) 86 | return nil, fmt.Errorf("%w: %v", ErrRequestReplyFailed, err) 87 | case err != nil: 88 | logger.Errorf("Request-Reply handler failed: %v", err) 89 | return nil, fmt.Errorf("%w: %v", ErrRequestReplyFailed, err) 90 | } 91 | 92 | if v := res.Header.Get(RequestReplyTerminateError); v != "" { 93 | return res.Data, fmt.Errorf("%s: %w", v, ErrTerminateTask) 94 | } 95 | 96 | if v := res.Header.Get(RequestReplyError); v != "" { 97 | return res.Data, errors.New(v) 98 | } 99 | 100 | return res.Data, nil 101 | } 102 | -------------------------------------------------------------------------------- /retrypolicy.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, R.I. Pienaar and the Project contributors 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package asyncjobs 6 | 7 | import ( 8 | "context" 9 | "fmt" 10 | "math/rand" 11 | "sort" 12 | "time" 13 | ) 14 | 15 | // RetryPolicy defines a period that failed jobs will be retried against 16 | type RetryPolicy struct { 17 | // Intervals is a range of time periods backoff will be based off 18 | Intervals []time.Duration 19 | // Jitter is a factor applied to the specific interval avoid repeating same backoff periods 20 | Jitter float64 21 | } 22 | 23 | // RetryPolicyProvider is the interface that the ReplyPolicy implements, 24 | // use this to implement your own exponential backoff system or similar for 25 | // task retries. 26 | type RetryPolicyProvider interface { 27 | Duration(n int) time.Duration 28 | } 29 | 30 | var ( 31 | // RetryLinearTenMinutes is a 50-step policy between 1 and 10 minutes 32 | RetryLinearTenMinutes = linearPolicy(50, 0.90, time.Minute, 10*time.Minute) 33 | 34 | // RetryLinearOneHour is a 50-step policy between 10 minutes and 1 hour 35 | RetryLinearOneHour = linearPolicy(20, 0.90, 10*time.Minute, 60*time.Minute) 36 | 37 | // RetryLinearOneMinute is a 20-step policy between 1 second and 1 minute 38 | RetryLinearOneMinute = linearPolicy(20, 0.5, time.Second, time.Minute) 39 | 40 | // RetryDefault is the default retry policy 41 | RetryDefault = RetryLinearTenMinutes 42 | 43 | retryLinearTenSeconds = linearPolicy(20, 0.1, 500*time.Millisecond, 10*time.Second) 44 | retryForTesting = linearPolicy(1, 0.1, time.Millisecond, 10*time.Millisecond) 45 | 46 | policies = map[string]RetryPolicyProvider{ 47 | "default": RetryDefault, 48 | "1m": RetryLinearOneMinute, 49 | "10m": RetryLinearTenMinutes, 50 | "1h": RetryLinearOneHour, 51 | } 52 | ) 53 | 54 | // RetryPolicyNames returns a list of pre-generated retry policies 55 | func RetryPolicyNames() []string { 56 | var names []string 57 | for k := range policies { 58 | names = append(names, k) 59 | } 60 | 61 | sort.Strings(names) 62 | 63 | return names 64 | } 65 | 66 | // RetryPolicyLookup loads a policy by name 67 | func RetryPolicyLookup(name string) (RetryPolicyProvider, error) { 68 | policy, ok := policies[name] 69 | if !ok { 70 | return nil, fmt.Errorf("%w: %s", ErrUnknownRetryPolicy, name) 71 | } 72 | 73 | return policy, nil 74 | } 75 | 76 | // IsRetryPolicyKnown determines if the named policy exist 77 | func IsRetryPolicyKnown(name string) bool { 78 | for _, p := range RetryPolicyNames() { 79 | if p == name { 80 | return true 81 | } 82 | } 83 | 84 | return false 85 | } 86 | 87 | // Duration is the period to sleep for try n, it includes a jitter 88 | func (p RetryPolicy) Duration(n int) time.Duration { 89 | if n >= len(p.Intervals) { 90 | n = len(p.Intervals) - 1 91 | } 92 | 93 | delay := p.jitter(p.Intervals[n]) 94 | if delay == 0 { 95 | delay = p.Intervals[0] 96 | } 97 | 98 | return delay 99 | } 100 | 101 | // RetrySleep sleeps for the duration for try n or until interrupted by ctx 102 | func RetrySleep(ctx context.Context, p RetryPolicyProvider, n int) error { 103 | timer := time.NewTimer(p.Duration(n)) 104 | 105 | select { 106 | case <-timer.C: 107 | return nil 108 | case <-ctx.Done(): 109 | timer.Stop() 110 | return ctx.Err() 111 | } 112 | } 113 | 114 | func linearPolicy(steps uint64, jitter float64, min time.Duration, max time.Duration) RetryPolicy { 115 | if max < min { 116 | max, min = min, max 117 | } 118 | 119 | p := RetryPolicy{ 120 | Intervals: []time.Duration{}, 121 | Jitter: jitter, 122 | } 123 | 124 | stepSize := uint64(max-min) / steps 125 | for i := uint64(0); i < steps; i += 1 { 126 | p.Intervals = append(p.Intervals, time.Duration(uint64(min)+(i*stepSize))) 127 | } 128 | 129 | return p 130 | } 131 | 132 | func (p RetryPolicy) jitter(d time.Duration) time.Duration { 133 | if d == 0 { 134 | return 0 135 | } 136 | 137 | jf := (float64(d) * p.Jitter) + float64(rand.Int63n(int64(d))) 138 | 139 | return time.Duration(jf).Round(time.Millisecond) 140 | } 141 | -------------------------------------------------------------------------------- /retrypolicy_test.go: -------------------------------------------------------------------------------- 1 | package asyncjobs 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo/v2" 5 | . "github.com/onsi/gomega" 6 | ) 7 | 8 | var _ = Describe("RetryPolicy", func() { 9 | Describe("Duration", func() { 10 | It("Should determine the correct interval with jitter", func() { 11 | b := RetryLinearOneMinute.Duration(10) 12 | d := RetryLinearOneMinute.Intervals[10] 13 | 14 | Expect(b).ToNot(Equal(d)) 15 | Expect(b).To(BeNumerically(">", float64(d)*0.5)) 16 | Expect(b).To(BeNumerically("<", float64(d)+float64(d)*0.5)) 17 | }) 18 | }) 19 | 20 | Describe("RetryPolicyNames", func() { 21 | It("Should have the right names", func() { 22 | Expect(RetryPolicyNames()).To(Equal([]string{"10m", "1h", "1m", "default"})) 23 | }) 24 | }) 25 | 26 | Describe("RetryPolicyLookup", func() { 27 | It("Should find the right policy", func() { 28 | _, err := RetryPolicyLookup("missing") 29 | Expect(err).To(MatchError(ErrUnknownRetryPolicy)) 30 | 31 | p, err := RetryPolicyLookup("1m") 32 | Expect(err).ToNot(HaveOccurred()) 33 | Expect(p).To(Equal(RetryLinearOneMinute)) 34 | }) 35 | }) 36 | 37 | Describe("IsRetryPolicyKnown", func() { 38 | It("Should report correct values", func() { 39 | Expect(IsRetryPolicyKnown("foo")).To(BeFalse()) 40 | Expect(IsRetryPolicyKnown("default")).To(BeTrue()) 41 | Expect(IsRetryPolicyKnown("1m")).To(BeTrue()) 42 | }) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /scheduled_task.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, R.I. Pienaar and the Project contributors 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package asyncjobs 6 | 7 | import ( 8 | "fmt" 9 | "time" 10 | 11 | "github.com/robfig/cron/v3" 12 | ) 13 | 14 | // ScheduledTask represents a cron like schedule and task properties that will 15 | // result in regular new tasks to be created machine schedule 16 | type ScheduledTask struct { 17 | // Name is a unique name for the scheduled task 18 | Name string `json:"name"` 19 | // Schedule is a cron specification for the schedule 20 | Schedule string `json:"schedule"` 21 | // Queue is the name of a queue to enqueue the task into 22 | Queue string `json:"queue"` 23 | // TaskType is the type of task to create 24 | TaskType string `json:"task_type"` 25 | // Payload is the task payload for the enqueued tasks 26 | Payload []byte `json:"payload"` 27 | // Deadline is the time after scheduling that the deadline would be 28 | Deadline time.Duration `json:"deadline,omitempty"` 29 | // MaxTries is how many times the created task could be tried 30 | MaxTries int `json:"max_tries"` 31 | // CreatedAt is when the schedule was created 32 | CreatedAt time.Time `json:"created_at"` 33 | } 34 | 35 | type ScheduleWatchEntry struct { 36 | Name string 37 | Task *ScheduledTask 38 | Delete bool 39 | } 40 | 41 | func newScheduledTaskFromTask(name string, schedule string, queue string, task *Task) (*ScheduledTask, cron.Schedule, error) { 42 | if name == "" { 43 | return nil, nil, ErrScheduleNameIsRequired 44 | } 45 | if !IsValidName(name) { 46 | return nil, nil, fmt.Errorf("%w: must match %s", ErrScheduleNameInvalid, validNameMatcher.String()) 47 | } 48 | if schedule == "" { 49 | return nil, nil, ErrScheduleIsRequired 50 | } 51 | if queue == "" { 52 | return nil, nil, ErrQueueNameRequired 53 | } 54 | 55 | cs, err := cron.ParseStandard(schedule) 56 | if err != nil { 57 | return nil, nil, fmt.Errorf("%w: %s", ErrScheduleInvalid, err) 58 | } 59 | 60 | sched := &ScheduledTask{ 61 | Name: name, 62 | Schedule: schedule, 63 | Queue: queue, 64 | TaskType: task.Type, 65 | Payload: task.Payload, 66 | MaxTries: task.MaxTries, 67 | CreatedAt: time.Now().UTC(), 68 | } 69 | 70 | if task.Deadline != nil { 71 | sched.Deadline = time.Until(*task.Deadline).Round(time.Second) 72 | if sched.Deadline < ShortedScheduledDeadline { 73 | return nil, nil, ErrScheduledTaskShortDeadline 74 | } 75 | } 76 | 77 | return sched, cs, nil 78 | } 79 | 80 | func newScheduledTask(name string, schedule string, queue string, taskType string, payload any, opts ...TaskOpt) (*ScheduledTask, cron.Schedule, error) { 81 | task, err := NewTask(taskType, payload, opts...) 82 | if err != nil { 83 | return nil, nil, err 84 | } 85 | 86 | return newScheduledTaskFromTask(name, schedule, queue, task) 87 | } 88 | -------------------------------------------------------------------------------- /task_scheduler_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, R.I. Pienaar and the Project contributors 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package asyncjobs 6 | 7 | import ( 8 | "context" 9 | "log" 10 | "sync" 11 | "time" 12 | 13 | "github.com/nats-io/jsm.go" 14 | "github.com/nats-io/nats.go" 15 | . "github.com/onsi/ginkgo/v2" 16 | . "github.com/onsi/gomega" 17 | ) 18 | 19 | var _ = Describe("TaskScheduler", func() { 20 | var ( 21 | ctx context.Context 22 | cancel context.CancelFunc 23 | wg sync.WaitGroup 24 | ) 25 | 26 | BeforeEach(func() { 27 | ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second) 28 | }) 29 | 30 | AfterEach(func() { cancel() }) 31 | 32 | Describe("Run", func() { 33 | It("Should function", func() { 34 | withJetStream(func(nc *nats.Conn, mgr *jsm.Manager) { 35 | client, err := NewClient(NatsConn(nc)) 36 | Expect(err).ToNot(HaveOccurred()) 37 | 38 | task, _ := NewTask("ginko:test", nil) 39 | Expect(client.NewScheduledTask("ginkgo", "@every 1s", "DEFAULT", task)).ToNot(HaveOccurred()) 40 | 41 | scheduler, err := NewTaskScheduler("ginkgo", client) 42 | Expect(err).ToNot(HaveOccurred()) 43 | scheduler.skipLeaderElection = true 44 | Expect(scheduler.Run(ctx, &wg)).ToNot(HaveOccurred()) 45 | scheduler.Stop() 46 | 47 | tasks, err := client.StorageAdmin().TasksInfo() 48 | Expect(err).ToNot(HaveOccurred()) 49 | log.Printf("msgs: %d", tasks.Stream.State.Msgs) 50 | Expect(tasks.Stream.State.Msgs).To( // depends when the test is started exactly 51 | And( 52 | BeNumerically(">=", uint64(4)), 53 | BeNumerically("<=", uint64(6)))) 54 | }) 55 | }) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /testdata/failing-handler.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "simulated failure" 4 | exit 1 5 | -------------------------------------------------------------------------------- /testdata/passing-handler.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "success" 4 | exit 0 5 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, R.I. Pienaar and the Choria Project contributors 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | //go:build tools 6 | 7 | package main 8 | 9 | import ( 10 | _ "github.com/onsi/ginkgo/v2/ginkgo/generators" 11 | _ "github.com/onsi/ginkgo/v2/ginkgo/internal" 12 | _ "github.com/onsi/ginkgo/v2/ginkgo/labels" 13 | ) 14 | 15 | // this file is here to make things like go generate and ginkgo install 16 | // happy, it has dependencies imported that it does not use and the build 17 | // constraint ensures it's excluded during normal builds. 18 | // 19 | // see https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module 20 | --------------------------------------------------------------------------------