Skip to content

v0.1.0

Choose a tag to compare

@github-actions github-actions released this 08 Apr 23:15
· 69 commits to main since this release
cbf804b

docs: add comprehensive API design documentation

docs: add schema versioning and migration systemAdded comprehensive documentation for handling schema evolution with three migration strategies: version-only (schema defaults), sparse record (breaking changes), and typed tuple (strict chain). Includes version resolution, job processing flow, migration pruning with since, and inspection API.

docs: add comprehensive implementation planAdded detailed implementation roadmap covering 12 phases from project skeleton to workflows, including tech stack (Node.js 20+, TypeScript, ioredis, Redis 7+), Redis key layout, source structure with Lua scripts, and phase-by-phase breakdown of core engine, schema validation, result handles, retries, context, events, stall detection, scheduling, migrations, inspector/DLQ, middleware, and workflows. Includes test strategy and milestone definitions for alpha

docs: refactor to explicit backend adapters with namespace typesChanged from implicit connection config to explicit adapter pattern. Core taskora package now has zero database dependencies - ioredis moved to peer dependency of taskora/redis adapter. All public types consolidated under Taskora namespace to avoid collisions. Added multi-entrypoint package structure with separate taskora/redis and future taskora/postgres adapters. Updated architecture diagrams, API examples, and implementation

docs: add serialization system and split storage optimizationAdded pluggable serializer interface with JSON default and optional MessagePack/CBOR adapters. Implemented split storage pattern: job metadata in ziplist-encoded hash (3-4x less memory), serialized data/result in separate String keys. All keys share hash tag for Redis Cluster safety. Updated implementation plan with split storage details in Lua scripts and key layout. Added serializers/ directory structure with peer dependency pattern

docs: add flow control features (debounce, throttle, dedup, TTL, singleton)Added comprehensive flow control documentation in API_DESIGN.md covering debounce (last-wins timer reset), throttle (per-key rate limiting with drops), deduplication (single job per key), TTL/expiration (time-sensitive jobs), singleton (global one-at-a-time), concurrency per key (group-based limits), and cron overlap prevention. Updated comparison table with Celery/BullMQ. Added Phase 12 to implementation plan with Lua scripts for

docs: add collect flow control (debounce with accumulation)Added collect pattern that debounces while accumulating all dispatched data into an array passed to handler. Includes three flush triggers: delay (debounce timer), maxSize (item count limit), maxWait (anti-infinite-debounce). Handler receives TInput[] instead of TInput when collect is configured. Implemented with Lua scripts for atomic push/flush operations, Redis keys for items list/metadata/flush timer, and typed overloads. Added

docs: add cancellation, test utilities, telemetry, and realtime UI featuresAdded graceful cancellation with distinct "cancelled" state (not failed), onCancel cleanup hooks, AbortSignal integration, and cascade cancellation for workflows. Added test utilities package with in-memory runner, time control, and zero Redis dependency. Added built-in OpenTelemetry with automatic spans and context propagation. Added realtime frontend hooks with SSE streaming and React useJobStatus hook. Updated comparison

chore: add project setup guide for Claude Code AgentAdded CLAUDE.md with comprehensive project overview including tech stack (TypeScript 5.x ESM, pkgroll, Biome, Vitest with testcontainers), package structure (multi-entrypoint with taskora/redis adapter), source layout, conventions (Taskora namespace, adapter abstraction, Lua scripts, split storage), commands, and Phase 0 implementation scope

and CI workflow. All source files are typed shells for Phase 1.

mode with NodeNext, Biome linting, Vitest + testcontainers test infra,

library. Set up multi-entrypoint package (. + ./redis), TypeScript strict

feat: reset repo to taskora — Phase 0 project skeletonReplace jobify (BullMQ wrapper) with taskora, a from-scratch task queue

all three connection modes.

job promotion, concurrent processing, bulk dispatch, shutdown, and

default. 13 integration tests covering happy path, failures, delayed

interval), graceful shutdown, and pluggable Serializer with json()

backoff polling, concurrency control, lock extension (30s TTL/10s

fallback, Task class (dispatch/dispatchMany), Worker with exponential

and XADD events, Redis backend with SCRIPT LOAD/EVALSHA + NOSCRIPT

feat: implement Phase 1 core engine — full job lifecycleAdapter interface (8 methods), 7 atomic Lua scripts with split storage

docs: update CLAUDE.md — Phase 1 complete, next Phase 2

back to handler signature. @standard-schema/spec as dev dep (types only).

(before ack). TInput/TOutput inferred from schema when provided, falls

feat: implement Phase 2 — Standard Schema validation + type inferenceInput validated on dispatch (before enqueue), output validated in worker

  • 10 new integration tests, all 36 tests passing

  • JobFailedError, TimeoutError error classes

  • Adapter: getState(), getResult(), getError() + updated enqueue(jobId)

  • Switch from INCR to client-side UUID v4 for job IDs

  • handle.getState() for lifecycle inspection

  • handle.result / handle.waitFor(ms) for polling job completion

feat: implement Phase 3 — ResultHandle + Job State- ResultHandle: sync dispatch, thenable (await = ensure enqueued)

docs: update CLAUDE.md — Phase 3 complete, next Phase 4

fixed/exponential/linear/custom strategies and ±25% jitter.

branching (retry→delayed vs permanent fail), backoff module with

maxDelay, jitter, retryOn/noRetryOn), ctx.retry() override, fail.lua

feat: implement Phase 4 — Retries with backoff strategiesDeclarative + manual retry support: RetryConfig (attempts, backoff,

docs: update CLAUDE.md — Phase 4 complete, next Phase 5

by default. handle.getProgress() and handle.getLogs() for querying.

Worker timeout races handler against setTimeout, TimeoutError not retried

feat: implement Phase 5 — Task Context (progress, logging, timeout)ctx.progress() and ctx.log.info/warn/error() as fire-and-forget writes.

docs: update CLAUDE.md — Phase 5 complete, next Phase 6

  • 12 integration tests (81 total)

  • subscribe() method on Adapter interface

  • Lazy duplicate Redis connection for subscriber

  • Background XREAD BLOCK stream reader with per-event enrichment

  • App-level events (task:completed, task:failed, task:active, worker:ready, worker:closing)

feat: implement Phase 6 — Events- Typed event emitter on tasks (completed, failed, retrying, progress, active)

result-handle tests: 9.1s → 0.8s

Periodic state fallback (2s) as safety net.

waitFor() calls. XREVRANGE snapshot + state check eliminates race.

Shared JobWaiter uses single XREAD BLOCK connection for all pending

perf: push-based ResultHandle via shared XREAD connectionReplace polling loop (50ms→500ms backoff) with push-based awaitJob().

  • Delayed jobs: ZADD LT sets marker to earliest due time

  • Worker: no backoff, BZPOPMIN-driven loop with 2s block timeout

  • Dedicated blocking connection per task (lazy, auto-cleanup)

  • moveToActive.lua: promote delayed + dequeue + re-add marker atomically

  • All Lua scripts (enqueue, enqueueDelayed, fail, nack) ZADD marker

  • Marker ZSET: score=0 for immediate, score=timestamp for delayed

Zero-latency job pickup — Redis wakes worker instantly on enqueue.

perf: BZPOPMIN marker-based blocking dequeue (BullMQ-style)Replace polling with backoff (50ms→1s) with marker ZSET + BZPOPMIN.

docs: update CLAUDE.md — Phase 6 complete, next Phase 7

maxCount } configurable per-task and via app defaults.

second. Stalled/failed events emitted via stream. stall: { interval,

30s). maxStalledCount (default 1) — re-queue on first stall, fail on

SREMs healthy jobs. Worker runs check on configurable interval (default

stalled), then seed active IDs for next check. extendLock.lua already

feat: implement Phase 7 — Stall DetectionTwo-phase stalledCheck.lua: resolve stalled candidates (no lock = truly

refactor: extract shared url() and waitFor() into tests/helpers.ts

docs: update CLAUDE.md — Phase 7 complete, next Phase 8

  • 8 unit + 12 integration tests (108 total)

  • 10 new Adapter methods, 3 Lua scripts, Redis schedule keys

  • app.schedules.list/pause/resume/update/remove/trigger

  • Overlap prevention, missed run catch-up/catch-up-limit:N

  • TICK_SCHEDULER Lua: atomic claim of due schedules

  • Scheduler class: leader election (SET NX PX), configurable poll loop

  • cron-parser as optional peer dep (dynamic import)

  • Duration template literal type with parseDuration()

feat: implement Phase 8 — Scheduling / Cron- app.schedule(name, config) + inline schedule on task definition

  • 9 integration tests (migrate, nack future, fail expired, inspect)

  • 15 unit tests (version resolution, normalization, chain execution)

  • MigrationStatus type, getVersionDistribution() on Adapter interface

  • VERSION_DISTRIBUTION Lua script (scan wait/active/delayed + HGET _v)

  • Inspector class with migrations(taskName) — version distribution query

  • into() type helper for tuple migrations (locks return type to schema)

  • resolveVersion() + normalizeMigrations() + runMigrations() in migration.ts

  • Schema validation AFTER migration (.default() values applied)

  • Migration chain: tuple (version derived) and record (sparse, explicit)

  • Worker version check: nack future (silent), fail expired (below since)

feat: implement Phase 9 — Schema Versioning & Migrations- dispatch() stamps _v = task.version on enqueue (was hardcoded 1)

docs: update CLAUDE.md — Phase 9 complete, next Phase 10

retryFromDLQ, retryAllFromDLQ, trimDLQ

Adapter additions: listJobDetails, getJobDetails, getQueueStats,

  • RETRY_DLQ, RETRY_ALL_DLQ, TRIM_DLQ Lua scripts

  • Configurable maxAge retention (trim piggybacks on stall check)

  • deadLetters.list/retry/retryAll with atomic Lua scripts

  • DeadLetterManager as view over existing :failed sorted set

Dead Letter Queue:

  • Timeline from ts → processedOn → finishedOn

  • LIST_JOB_DETAILS Lua script: 1 RTT per task (LRANGE + HMGET + GET)

  • find(jobId) cross-task search, find(task, jobId) typed variant

  • stats() — LLEN/ZCARD pipeline, per-task or aggregated

  • active/waiting/delayed/failed/completed() queries with limit/offset

feat: implement Phase 10 — Inspector + Dead Letter QueueInspector expansion:

  • trimDLQ maxAge retention cleanup

  • deadLetters.retryAll() bulk re-enqueue

  • deadLetters.retry() with and without task name

  • find() with logs, timeline, typed variant

  • list queries (waiting, completed, failed, delayed, pagination)

  • stats per-task and cross-task aggregation

test: Inspector queries, stats, find, DLQ list/retry/retryAll/trim16 integration tests covering:

docs: update CLAUDE.md — Phase 10 complete, next Phase 11

  • 10 integration tests (app-level, per-task, ordering, data/result, errors)

  • 9 unit tests (compose order, errors, data/result mutation, next() guard)

  • Composition happens once per Worker at construction (not per job)

  • Pipeline: deserialize → migrate → validate → [middleware chain → handler]

  • MiddlewareContext extends Context with task, mutable data, mutable result

  • Execution order: app middleware → task middleware → handler

  • Task option middleware: [fn, fn] per-task middleware

  • app.use(middleware): app-level middleware, chainable, throws after start()

  • src/middleware.ts: compose() function (onion model, next()-called-twice guard)

feat: implement Phase 11 — MiddlewareKoa-style composable middleware for cross-cutting concerns:

docs: update CLAUDE.md — Phase 11 complete, next Phase 12

12c: Collect / Batch Accumulator (debounce + accumulator pattern)

12b: TTL, Singleton, Concurrency per Key, Cron overlap (worker-time)

docs: split Phase 12 into 12a/12b/12c in IMPLEMENTATION.md12a: Debounce, Throttle, Deduplication (dispatch-time Lua scripts)

  • Adapter interface: debounceEnqueue, throttleEnqueue, deduplicateEnqueue

  • ResultHandle: enqueued (bool|null), existingId (dedup redirect)

  • ThrottledError, DuplicateJobError (opt-in via throwOnReject)

feat: add flow control types, errors, and ResultHandle.enqueued- DispatchOptions: debounce, throttle, deduplicate, throwOnReject

  • Task.dispatch(): routes to flow control adapter methods

  • RedisBackend: debounceEnqueue, throttleEnqueue, deduplicateEnqueue

  • ACK/FAIL extended: clean dedupKey from job hash on completion/failure

  • DEDUPLICATE_ENQUEUE: check dedup key + job state, skip if exists

  • THROTTLE_ENQUEUE: counter with PX window, drop if over limit

feat: implement debounce, throttle, deduplication Lua scripts and adapter- DEBOUNCE: replace previous delayed job atomically, debounce key with PX

  • Dedup: existing handle, redirect result, key clears on complete/fail, while option, throwOnReject

  • Throttle: max limit drops excess, window reset, throwOnReject, independent keys

test: flow control — debounce, throttle, deduplication (13 tests)- Debounce: 5 rapid dispatches only last runs, data replacement, independent keys

  • keys.ts: new expired sorted set key

  • QueueStats gains expired field, listJobDetails accepts "expired" state

  • Adapter interface updated: enqueue/dequeue accept flow control options

  • TtlConfig, DequeueOptions interfaces in Taskora namespace

  • JobState gains "expired", DispatchOptions gains ttl/concurrencyKey/concurrencyLimit

feat: add TTL, singleton, concurrency-per-key types and ExpiredError- ExpiredError class in errors.ts

getQueueStats includes expired ZCARD

  • Backend: updated eval calls, dequeue accepts DequeueOptions,

  • DEBOUNCE/THROTTLE/DEDUPLICATE: pass through flow control fields

  • FAIL/NACK/STALLED_CHECK: DECR concurrency counter on job leaving active

  • ACK: KEYS[4]=marker, DECR concurrency counter, wake workers

    concurrency-per-key INCR/block loop (up to 100 attempts)

  • MOVE_TO_ACTIVE: TTL check on promote + dequeue, singleton LLEN guard,

feat: implement TTL, singleton, concurrency-per-key Lua scripts and adapter- ENQUEUE/ENQUEUE_DELAYED: store expireAt, concurrencyKey, concurrencyLimit

  • index.ts: export ExpiredError

  • Inspector: expired() list method, stats includes expired, timeline handles expired state

  • App: TaskOptionsBase gains singleton/concurrencyLimit/ttl, parses Duration

  • Worker: builds DequeueOptions from task config, passes to blockingDequeue

  • Task.dispatch: computes expireAt from TTL, resolves concurrencyLimit

feat: wire TTL, singleton, concurrency-per-key through core + inspector- TaskConfig: singleton, concurrencyLimit, ttl (maxMs + onExpire)

counter released on failure, different keys independent

  • Concurrency per key: per-key limit respected, dispatch override,

  • Singleton: second job waits for first, non-singleton concurrent

    discard mode, delayed expiration, inspector stats

test: TTL expiration, singleton, concurrency per key (12 tests)- TTL: expired not processed, dispatch override, non-expired OK,

docs: update CLAUDE.md — Phase 12a/12b complete, next Phase 12c

feat: add CollectConfig type and collectPush to Adapter interface

CollectTaskOptions<I,O> overload on app.task().

RedisBackend.collectPush(), Task.dispatch() collect path,

HDEL collectKey after drain to prevent double-drain on retry.

moveToActive.lua: drain collect buffer into :data at claim time,

flush sentinel in delayed set, inline maxSize immediate flush.

feat: implement collect (batch accumulator) — Lua, adapter, coreCOLLECT_PUSH Lua script: accumulate items per key, manage debounce

test: collect — debounce flush, dynamic key, maxSize, maxWait, retry (5 tests)

  • Redis keys: cancelled sorted set, cancel pub/sub channel

  • listJobDetails accepts "cancelled" state

  • extendLock return type: "extended" | "lost" | "cancelled"

  • Adapter: cancel(), finishCancel(), onCancel() methods

  • QueueStats.cancelled field

  • CancelledEvent type, TaskEventMap.cancelled, AppEventMap.task:cancelled

feat: add CancelledError, cancelled types, and Adapter cancel interface- CancelledError class (distinct from JobFailedError)

  • Active cancel tests: 10s → ~80ms

  • worker.start() now async — awaits subscription before polling

  • extendLock cancel detection retained as fallback safety net

  • controller.abort("cancelled") fires immediately on pub/sub message

  • Worker subscribes to cancel channel on start() via adapter.onCancel()

feat: instant cancel via Redis pub/sub — sub-100ms active job cancellation- cancel.lua PUBLISHes jobId to cancel channel for active jobs

docs: update CLAUDE.md — Phase 13 (Graceful Cancellation) complete

16 unit tests (no Docker needed).

Entrypoint: taskora/memory — memoryAdapter() factory.

debounce/throttle/dedup, collectPush, inspector, DLQ, scheduling.

dequeue, ack/fail/nack, extendLock, cancel/finishCancel, stalledCheck,

data structures (arrays, Maps, sorted arrays). All ~40 methods: enqueue,

feat: in-memory Adapter (taskora/memory) — zero-Redis queue backendMemoryBackend implements full Taskora.Adapter interface using plain JS

inspector, execute with auto-retry, from-app with multi-task chains).

25 unit tests (run, retry, middleware, dispatch, delay, cancel, collect,

  • MemoryBackend.getEarliestDelayedScore() for auto-retry advancement

  • App.registerExternalTask() / getRegisteredTasks()

  • Task._patchDeps() — swap adapter deps, returns restore fn

Internal:

  • dispatch/advanceTime/processAll/flush/clear/dispose

    use memory backend; interconnected task chains work in-memory

  • createTestRunner({ from: app }) — patch all tasks on existing app to

  • importTask(task) — copy production task to memory adapter

    returns {result, state, attempts, logs, progress, error, handle}

  • execute(task, data) — full queue pipeline with auto-retry advancement,

  • run(task, data) — direct handler execution with inline retry loop

API:

feat: TestRunner (taskora/test) — run, execute, importTask, from-app modecreateTestRunner() — in-memory test runner for unit testing without Redis.

docs: update CLAUDE.md — Phase 14 (Test Utilities) complete, Phases 15–16 deferred

  • Adapter additions: trimCompleted()

  • trimDLQ.lua + trimCompleted.lua: two-phase (age-based + count-based), batch 100

  • retention.failed: { maxAge: "7d", maxItems: 300 }

  • retention.completed: { maxAge: "1h", maxItems: 100 }

refactor: retention — auto-trim completed/failed sets with configurable limitsReplace single DLQ maxAge with full retention config:

when user adds task.on("failed") or app.on("task:failed").

job ID, attempt info, and retry status. Automatically suppressed

taskora logs to console.error with full stack trace, task name,

feat: default error logging — console.error with stack when no failed listenerWhen a task handler throws and no failed listener is registered,

  • Singleton: max 1 concurrent across 3 pods (6 jobs)

  • Stall recovery: pod-2 recovers stalled job from crashed pod-1

  • Cross-pod onCancel hook fires on the processing pod

  • Cross-pod cancel: cancel from pod-1 aborts active job on pod-2

  • Event fan-out: all pods receive completed event (XREAD, not XREADGROUP)

  • Dispatch from any pod, process on any pod

test: multi-instance integration tests — 8 tests across 5 scenarios- Job distribution across 3 pods without duplicates (50 jobs)

  • CLAUDE.md: convention — sync documentation/ with code changes

  • events.md: multi-instance fan-out behavior, default error logging note

docs: default error logging + multi-instance event fan-out in VitePress- error-handling.md: default console.error with stack, auto-suppression

  • API reference (manual), comparison page

  • Recipes: email queue, image processing, webhook delivery, rate-limited API, cron cleanup

  • Testing: overview, run vs execute, virtual time, patterns

  • Operations: inspector, DLQ/retention, stall detection, monitoring

  • Features: retry/backoff, TTL, cancellation, middleware, events, scheduling, flow control, batch processing, versioning

  • Guide: getting started, core concepts, tasks, dispatching, context, adapters, serializers, error handling

docs: VitePress documentation site — guide, features, operations, testing, recipesFull documentation site with VitePress:

  • .gitignore: exclude generated api/ subdirs, .vitepress/cache, .vitepress/dist

  • typedoc sidebar loaded dynamically in VitePress config

  • vitepress-plugin-llms: generates llms.txt and llms-full.txt for LLM indexing

  • build script: gen:typedoc runs before vitepress build

  • scripts/generate-typedoc.ts: run typedoc, rename module dirs, fix links/sidebar

feat: typedoc API reference + vitepress-plugin-llms- typedoc + typedoc-plugin-markdown + typedoc-vitepress-theme for auto-generated API docs

  • Hero image added to landing page

  • logo.svg: lucide:workflow (stroke=currentColor) for nav + hero

  • favicon.svg: lucide:workflow on violet rounded square

  • Hero gradient text + background glow

style: violet brand palette + workflow icon + hero image- Brand color: #3b7dff → #7c3aed (violet) with proper dark mode variant

  • favicon.svg: violet background, white nodes, checkmark — reads at 32x32

  • nav-logo.svg: compact version for header navigation

    connected by curved paths with flow dots, checkmark on result node

style: custom SVG logo — dispatch → process → result flow diagram- logo.svg: hero image — three gradient nodes (dispatch, process, result)

in index.md, getting-started.md, scheduling.md

feat: vitepress-plugin-package-managers — auto bun/npm/pnpm/yarn tabsReplace manual ::: code-group install blocks with ::: pm-add

  • virtual:group-icons.css import

  • vitepress-plugin-group-icons for tab icons (bun/npm/pnpm/yarn logos)

fix: package-managers client registration + group-icons for tab icons- enhanceAppWithPackageManagers(app, { icons }) in theme — fixes empty render

  • base URL configurable via BASE_URL env var

  • .github/workflows/docs.yml: build library → gen:typedoc → vitepress build → deploy

  • vitepress-plugin-group-icons for tab icons

feat: navbar package manager switcher + GitHub Pages deployment- PackageManagerSwitch in nav-bar-content-after slot

Update README.md

fix: set BASE_URL=/taskora/ for GitHub Pages subpath deployment

Merge branch 'main' of https://github.com/kravetsone/taskora

  • Exported from taskora: chain, group, chord, Signature, WorkflowHandle

  • Late-bound dispatch pattern to break circular imports

  • Workflow types: WorkflowState, WorkflowAdvanceResult, WorkflowFailResult

  • Adapter interface: 6 new workflow methods + _wf/_wfNode enqueue fields

  • Task: .s(), .map(), .chunk(), _getDeps()

  • WorkflowHandle — thenable, .result, .cancel(), .getState()

  • flattenToDAG() — any composition tree → flat DAG (nodes + deps + terminals)

  • chord() — parallel header + callback with typed fan-in

  • group() with tuple output inference (InferOutputTuple)

  • chain() with 10 type-safe overloads, .pipe() for unlimited chaining

  • Signature class: task.s(data) / task.s() — serializable task snapshot

feat: add workflow composition layer (Signature, chain, group, chord)Phase 17a core — Celery-inspired Canvas with full TypeScript type safety.

  • enqueue() stores _wf/_wfNode in job hash when present

  • ENQUEUE Lua: extended with ARGV[11]/ARGV[12] for _wf/_wfNode fields

  • RedisBackend: same 6 methods using Redis hash (taskora:wf:{id})

  • failWorkflow: cascade — mark all pending/active nodes failed

    input (single dep → direct, N deps → JSON array)

  • advanceWorkflow: mark node done, find ready dependents, compute

  • Workflow state stored in Map with per-node state/result tracking

    getWorkflowState, cancelWorkflow, getWorkflowMeta

feat: implement workflow adapter methods (Memory + Redis)- MemoryBackend: createWorkflow, advanceWorkflow, failWorkflow,

  • Workflow failure + cascade

  • Workflow state tracking, runner.steps, task.map()

  • Chain/group/chord/nested execution end-to-end

  • DAG flattening (6 topologies incl. nested compositions)

  • chain/group/chord construction + validation

  • Signature creation, bound data, .pipe()

Tests (28 new):

  • clear()/dispose() reset workflow steps

  • processOne: advance/fail workflow after job ack/fail

  • runner.steps tracks workflow step execution history

TestRunner:

  • Reads _wf/_wfNode from job hash via getWorkflowMeta()

  • failWorkflow() after permanent fail — cascade cancel active jobs

  • advanceWorkflow() after successful ack — enqueue ready nodes

feat: integrate workflows into Worker and TestRunnerWorker:

  • CLAUDE.md: updated source layout, phase status, Phase 17a summary

  • Features table: added Workflows row

  • Sidebar: added Workflows to Composition section

    WorkflowHandle, TTL, DAG internals, testing guide

    Signatures, Chain, Group, Chord, Composability, Map/Chunk,

  • documentation/features/workflows.md: VitePress feature page —

    Split into 17a (core primitives) and 17b (durable steps, wait-for-event)

    adapter additions, type system, worker integration, Lua scripts.

docs: Phase 17a workflow design + user-facing documentation- docs/IMPLEMENTATION.md: full Phase 17a design — DAG model, Redis layout,

Updated all test files (19 files) to use createTaskora.

No backward compat alias needed (not yet published).

avoids generic app naming and conflicts with other frameworks.

refactor: rename taskora() → createTaskora()Factory function renamed for clarity — const taskora = createTaskora({...})

  • Workflow patterns: when to use chain/group/chord, error boundaries

  • Handler patterns: ctx.log, ctx.signal, idempotency, guard clauses

  • Project structure: taskora.ts, tasks/ by domain, worker.ts

  • Naming: createTaskora(), *Task suffix, kebab-case task names

docs: add Conventions & Best Practices guideNew page /guide/conventions covering:

  • Updated 25+ doc pages (guide, features, operations, recipes)

  • taskora.task(), taskora.start(), taskora.close() everywhere

  • Task variables: *Task suffix (sendEmailTask, processImageTask, etc.)

docs: apply conventions across all documentation- All examples: taskora() → createTaskora(), const app → const taskora

with status polling, order fulfillment chain

  • REST API (Elysia): route → workflow dispatch, long-running task

    broadcasts, bot command → task dispatch pattern

docs(conventions): add real-world integration examples- Telegram bot (GramIO): task queue for rate-limited messages,

No separate workflows/ directory needed.

docs(conventions): merge workflows into tasks/ directoryWorkflows are compositions of tasks — keep them in the same folder.

  • "From App Mode" → "From Instance Mode" in testing (consistent with instance)

  • "app events" → "events" in monitoring (taskora variable used, not app)

  • "Create Your App" → "Create Your Instance" (matches instance terminology)

docs: fix inconsistent terminology across documentation- "longTaskTask" → "longRunningTask" in cancellation example

Backend normalizes cjson empty table ({}) → [] for array fields.

All scripts operate on a single workflow hash key — Cluster-safe.

  • CANCEL_WORKFLOW: mark all pending/active nodes, collect active job IDs

  • FAIL_WORKFLOW: mark node + workflow failed, collect active job IDs

  • ADVANCE_WORKFLOW: mark done, find ready deps, compute input, check terminals

  • CREATE_WORKFLOW: HMSET graph + initial node states

refactor: atomic Lua scripts for workflow state transitionsReplace inline Redis HGET/HSET calls with 4 atomic Lua scripts:

  • Redis atomicity: verify hash structure, _wf/_wfNode in job hash

  • task.map(): parallel batch dispatch

  • Cancel: pending steps not executed after cancel

  • Failure: permanent fail cascades to workflow

  • Workflow state: running → completed transition

  • Nested: chord of chains

  • Chord: group + callback fan-in

  • Group: parallel execution with tuple result

  • Chain: 2-step, 3-step, .pipe() syntax

test: workflow integration tests with real Redis12 tests covering end-to-end workflow execution:

workflow DAG visualization, and 4-phase implementation plan.

(Hono + React SPA + SSE), REST API surface, 8 UI views including

docs: add taskora/board dashboard design documentComprehensive design for in-house admin panel covering architecture

  • hono >= 4 as optional peer dependency

  • CLEAN_JOBS Lua script for bulk sorted set cleanup

  • Throughput counters: INCR per-minute buckets in ack/fail Lua scripts

    getThroughput methods

  • Adapter: cleanJobs, getServerInfo, listWorkflows, getWorkflowDetail,

Backend additions:

  • Dark/light/auto theme, keyboard shortcuts, global search

  • Migrations insight with version distribution chart

  • DLQ view with error frequency grouping

  • Schedule management: pause/resume/trigger/delete

  • Workflow DAG visualization (@xyflow/react) with node state colors

  • Job detail with timeline, data/result/error/logs tabs, progress bar

  • Task detail with state tabs, job table, bulk retry/clean

  • Overview dashboard with stat cards + throughput chart (Recharts)

  • React 19 + Tailwind + Vite SPA, pre-built at publish time

UI (ui/ → src/board/static/):

  • Universal handler: works with Express, Fastify, Bun, Deno

  • Auth middleware hook, read-only mode, field redaction

    throughput endpoints + SSE real-time event stream

  • Hono REST API: overview, jobs, schedules, workflows, DLQ, migrations,

Server (src/board/):

the taskora runtime.

feat: taskora/board — admin dashboard with Hono + React SPAPhase 18: full-featured admin panel for observing tasks and managing

  • Add examples/board-demo.ts with 4 sample tasks + schedules

  • SPA fallback: non-file routes serve index.html

    resolution (source dev, dist published) instead of hono/bun serveStatic

fix: board static file serving + add board-demo example- Rewrite static serving to use fs.readFileSync with multi-path

  • Rewrite static file serving with multi-path fs resolution

  • keepPreviousData on all queries to prevent "Loading..." flash

  • All polling intervals reduced to 30s (safety net, not primary)

  • Job events soft-invalidate (background only, no loading state)

fix: SSE-first updates, eliminate polling flicker- SSE stats:update events directly patch query cache (no refetch)

starts with /api/ and calls next() to let the API handler respond.

returning index.html instead of JSON. Now checks if the path

fix: static handler must skip /api/ routesThe wildcard static handler was intercepting API requests and

/board/api/... Derive absolute base from window.location.pathname.

/board/jobs/abc — fetch resolved to /board/jobs/api/... instead of

fix: use absolute base path for API calls in productionRelative BASE='.' broke when SPA navigated to nested routes like

absolute paths that match the board mount point.

Vite base was './' (relative). Changed to '/board/' so HTML references

fix: set Vite base to /board/ for correct asset pathsAssets were loaded from /assets/ instead of /board/assets/ because

  • Job list shows "retried" badge + amber attempt count

  • Retrying states annotated "attempt failed, scheduling retry"

  • Retry-completed jobs shown in amber with "after N retry" label

  • Vertical timeline with colored dots per state

Timeline:

  • Avg keys/task and avg memory/task displayed on Overview

  • Expanded: peak memory, db size, connected clients

Redis info:

  • Per-task stacked area chart on Overview

  • getThroughput aggregates across tasks or reads per-task

  • Per-task metric keys: taskora:metrics:TASK:completed|failed:BUCKET

  • Move metric INCR from Lua to TypeScript (avoids hash tag issues)

fix: throughput metrics + per-task charts + better timelineThroughput:

  • Migrations link on task detail header

    (Lua prefix had hash tags that broke key pattern)

  • Fix throughput: moved INCR from Lua to TS ack/fail methods

  • Completed-after-retry shown as amber badge (no text, just color)

  • State tab counts from live queue stats

  • Redis keys + memory stats displayed per task

  • Per-task throughput chart on TaskDetail page

feat: per-task throughput, key count, memory on task detail page- getTaskKeyStats adapter method: SCAN key count + MEMORY USAGE estimate

  • 3 schedules: daily report, weekly report, sync every 30m

    group (parallel resize 3 sizes), chord (3 resizes → aggregate)

  • Workflows: chain (resize→watermark→upload→notify),

  • resize/watermark/upload-cdn/notify/aggregate: workflow steps

  • generate-report: slow, reliable

  • webhook-delivery: ultra flaky (60% fail), fills DLQ fast, has TTL

  • sync-data: dispatched with delays, very flaky (35% fail)

  • process-image: slow (builds active), moderate failures

demo: rich board demo with 10 tasks, workflows, diverse states- send-email: burst dispatch, fast, rarely fails

  • Interval formatting (ms → human readable)

  • Delete button in expanded view

  • "View all X jobs" link to task detail page

  • Last execution: job link + state badge + result/error preview

  • Shows: type, expression, overlap policy, missed policy, dispatch data

feat: expandable schedule cards with execution detail- Click schedule row to expand detail panel

resize-and-aggregate

  • Demo uses named workflows: image-pipeline, multi-size-resize,

    terminalTasks[]

  • listWorkflows returns tasks[] (all unique task names) instead of

  • Workflow detail header shows name prominently

    state badge, node count, relative time

  • Workflow list: card layout showing name, all tasks (not just terminal),

  • Name stored in workflow graph, displayed in list + detail

    chord(resize, resize, resize → aggregate)

    group(resize, resize, resize)

    chain(resize → watermark → upload-cdn → notify)

  • Auto-generated description from composition structure:

feat: workflow names + improved workflow list UI- WorkflowDispatchOptions.name — optional human-readable name

error analysis) and advantages (workflows, schedules, SSE, themes).

and bull-board. Identifies gaps (worker management, Prometheus,

docs: board ideas pool + Flower comparison20 ideas in 3 tiers, side-by-side comparison with Celery Flower

docs: add public /taskora skill for Claude Code

docs: add internal flows, state diagrams, and best practices to /taskora skill

docs: add AI Skills documentation page with installation guide

docs: expand AI Skills page with vitepress-plugin-llms usage guide

docs: add Further reading section to /taskora skill with llms.txt links

docs: add metadata.json manifest and frontmatter metadata for /taskora skill

scripts, pipelines, HGETALL, INFO, scheduled metrics).

driver end-to-end (enqueue, dequeue, BZPOPMIN blocking, ack, Lua

refactored ioredis path. A manual smoke test exercises the Bun

All 300 existing integration tests pass unchanged against the

                     and Sentinel unsupported

                     forced at connect, HGETALL normalized, Cluster
  • taskora/redis/bun Bun runtime only, routes via .send(), HELLO 2

  • taskora/redis/ioredis explicit ioredis entry

  • taskora/redis re-exports ioredis driver (no breaking change)

New exports:

~25 dumb commands route through a generic command() escape hatch.

blocking commands, pipelines, pubsub, connection management). The

covering the small non-trivial surface (Lua + NOSCRIPT recovery,

~67 Adapter methods are shared — the driver exposes 10 methods

All taskora state-machine logic, 27 Lua scripts, key layout, and

Bun.RedisClient (runtime built-in, zero peer deps under Bun).

implementations: ioredis (default, for Node/Cluster/Sentinel) and

feat(redis): add Bun.RedisClient backend via driver abstractionSplit the Redis adapter into a thin RedisDriver interface + two

Integration suite: 49.88s, scheduler suite: 40.8s → 12.4s.

client still uses close() for a clean QUIT.

use it for all clients that may be parked in a blocking command. Main

(ioredis: disconnect(false); Bun: close() — already non-graceful) and

Add a disconnect() method to RedisDriver for forceful termination

the full block timeout (~2s). Scheduler suite went from 12s to 41s.

BZPOPMIN / XREAD BLOCK / SUBSCRIBE, so every test teardown paid up to

onCancel sub-clients. QUIT can't be sent while a connection is stuck in

→ client.quit() for blocking-dequeue, event-reader, job-waiter, and

fix(redis): force-disconnect blocking clients on shutdownThe driver refactor replaced client.disconnect(false) with driver.close()

Bun-runtime global).

Deno + BunDriver is intentionally not supported (Bun.RedisClient is a

  • Deno 2.7 + taskora/redis/ioredis (via deno run -A --node-modules-dir=auto npm:vitest/vitest.mjs run)

  • Bun 1.3 + taskora/redis/bun

  • Bun 1.3 + taskora/redis/ioredis

  • Node 20 + taskora/redis/ioredis

Full 300-test suite now passes on:

Result

identically on all runtimes. Test logic unchanged.

array, string etc. directly, so the namespace import works

import * as z from "zod" — zod's top-level exports include object,

Fix: switch the four test files that use z.object(...) etc. to

schema.test.ts (4 tests) fail at module load time on Bun.

comes through as undefined. Made migration.test.ts (7 tests) and

Vitest the ESM↔CJS interop drops the re-export and the named z

Under Node's Vitest this binding survives module loading; under Bun's

(export { z } after import * as z from "./v4/classic/external.js").

Zod 4.x re-exports itself as a named z namespace from the ESM entry

Zod + Bun + Vitest: import { z } from "zod" is undefined

driver's pipeline serializes the eval calls first.

display order matches client-side dispatch order regardless of which

job hash. LIST_JOB_DETAILS for lrange mode sorts by (ts, seq) so

starts. Pass both through to adapter.enqueue which stores them in the

synchronously at the task.dispatch() call site, before the async IIFE

Fix: capture ts (wall-clock ms) and a process-monotonic seq

so ts/seq reflected Redis-arrival order, not client call order.

Root cause: dispatch order was captured per-driver after async yields,

jobs in raw LIFO order, and the test failed deterministically.

FIFO expectation. BunDriver exposes pure LPUSH semantics, which put

serialize parallel dispatches in an order that matched the test's

ioredis only because ioredis's internal command pipeline happened to

tests/integration/inspector-dlq.test.ts > waiting() passed under

Taskora: inspector.waiting() was latently flaky (1 test)

explain the leniency as a deliberate cross-driver normalization.

Fix: coerceArgs() now matches ioredis — undefined/null → "". Docs

active.

then skipped every subsequent dispatch because lastJobId was forever

which stalled jobs in active state. Scheduler (overlap=false default)

"TypeError: The first argument must be of type string, Buffer, ..."

string; BunDriver's strict coerceArgs() threw

void handler results. ioredis silently coerces undefined to empty

Worker.ack() passes JSON.stringify(undefined) → actual undefined for

BunDriver: coerceArgs throws on undefined (10 tests)

command() / blockingZPopMin() don't double-send.

working. Serialized behind a shared in-flight Promise so racing

used purely for subscribe() never receives HELLO 2 and pub/sub keeps

evalSha / blockingZPopMin / blockingXRead / pipeline exec). A client

issue HELLO on the first command-ish call (command / scriptLoad /

connect(), but duplicated clients start with helloSent=false and only

Fix: make HELLO 2 lazy. The main driver still negotiates eagerly in

and multi-instance cross-pod cancel.

reach the listener. This kills cancel.test.ts active-job cancellation

publish count is correct (num subscribers = 1), but messages never

handler once HELLO 2 has been negotiated on the connection — the

Bun's RedisClient.subscribe() silently stops invoking the message

BunDriver: HELLO 2 breaks pub/sub push delivery (4 tests)

guarantee parity.

real bugs and one latent flake that all needed fixing before CI could

BunDriver. Running the real 300-test suite cross-runtime surfaced four

fix(redis): make full integration suite pass on Node/Bun/Deno × ioredis/BunThe driver-abstraction refactor (cc0b54b) was only smoke-tested against

invocations.

and don't drift across deno run -A --node-modules-dir=auto npm:vitest/...

deno.lock is committed so Deno-based CI runs pin the exact npm dep set

Bun.RedisClient is a Bun-runtime global).

per runtime, and the known-skip rule (Deno × BunDriver, since

adapters. Records which combos are supported, the test-runner invocation

audit across Node 20 / Bun 1.3 / Deno 2.x and the ioredis / BunDriver

docs(testing): add cross-runtime testing matrix and deno.lockTESTING_MATRIX.md captures the empirical results of the Phase 1 compat

opaque duck-typing failure downstream.

Node surfaces a clear actionable error from the helper instead of an

guards described above. Attempting TASKORA_TEST_DRIVER=bun under

The 2 skipped tests under BunDriver are the ioredis-constructor-form


Deno + ioredis       deno run -A --node-modules-dir=auto npm:vitest/vitest.mjs    300/300

Bun  + BunDriver     TASKORA_TEST_DRIVER=bun bunx --bun vitest    298 passed | 2 skipped

Bun  + ioredis       bunx --bun vitest    300/300

Node + ioredis       vitest run           300/300

Verified on all four combos

local Redis without paying Docker startup per invocation.

workflow, and it also lets developers iterate against a persistent

jobs do when they provision Redis via services: redis: in the

already set, skipping testcontainers spin-up. This is what CI matrix

tests/setup.ts now returns early if process.env.REDIS_URL is

Setup honors pre-existing REDIS_URL

path (threads pool diverges between Node and Bun's Worker impl).

only combination that runs reliably under Bun's vitest-via-node-compat

pool: forks + singleFork: true is preserved on both — it's the

  • integration keeps globalSetup: tests/setup.ts and longer timeouts

  • unit runs ~125 tests in ~1s (no Docker, no testcontainers startup)

vitest.config.ts now defines unit and integration projects:

Vitest projects split

factory detection.

under TASKORA_TEST_DRIVER=bun instead of throwing from duck-typed

These are now guarded with it.skipIf(!isIoredis) so they remain green

(redisAdapter({ host, port }) and redisAdapter(new Redis(url()))).

Two lifecycle tests exercise ioredis-specific constructor forms

Driver-specific test guards

mechanical sed — no test logic touched.

../../src/redis/index.js to ../create-adapter.js. The swap is a

All 17 integration test files had their import path moved from

Bulk import swap

call-sites keep the familiar synchronous redisAdapter(url()) shape.

TASKORA_TEST_DRIVER (default: ioredis), using top-level await so

src/redis/ioredis.js or src/redis/bun.js based on

tests/create-adapter.ts module re-exports redisAdapter from either

site, without touching individual test files' logic. A new

test: parameterize integration suite by TASKORA_TEST_DRIVER env varThe integration suite now runs against any driver selected at the call

These double as local-dev shortcuts for reproducing any CI cell.

test:all - sequential run of all four combos

test:deno - deno run -A npm:vitest/vitest.mjs run

test:bun:ioredis - bunx --bun vitest run (Bun runtime + ioredis)

test:bun - TASKORA_TEST_DRIVER=bun bunx --bun vitest run

test:node:integration- only the integration project

test:node:unit - only the unit project (no Redis)

test:node - vitest run under Node

Added:

package.json scripts

matrix would be waste).

once (biome + pkgroll are runtime-agnostic, duplicating across the

Lint + build are factored into a separate lint-build job that runs

Docker-in-Docker and all four runtimes hit the same shared Redis.

pre-set URL and skips its own testcontainers spin-up — so CI avoids

REDIS_URL=redis://localhost:6379. tests/setup.ts honors this

Uses a GitHub Actions services: redis:7-alpine sidecar and pre-sets

Redis provisioning

  • deno → deno run -A --node-modules-dir=auto npm:vitest/vitest.mjs run

        Node, which is what the old workflow silently did)
    
        without it `bunx` follows vitest's shebang and runs under
    
  • bun → bunx --bun vitest run (the --bun flag is critical —

  • node → npx vitest run

  1. Runs vitest via the runtime-appropriate invocation:

    tests/create-adapter.ts picks the right factory at module load.

  2. Sets TASKORA_TEST_DRIVER=${{ matrix.driver }} so

    node_modules tree is interchangeable).

    manager across all cells — ioredis has no native addons, so the

  3. Installs deps via bun install --frozen-lockfile (one package

Each cell:

another — the whole point is to see where the runtimes actually differ.

fail-fast: false so one failing cell doesn't hide divergence in

Matrix shape

Bun-runtime global.

Deno + BunDriver is intentionally excluded: Bun.RedisClient is a

  • Deno + taskora/redis/ioredis (ioredis under Deno's npm interop)

  • Bun + taskora/redis/bun (Bun's native RedisClient)

  • Bun + taskora/redis/ioredis (ioredis under Bun's node-compat)

  • Node + taskora/redis/ioredis (golden baseline)

every supported runtime × driver combination on every PR:

Node, not Bun — --bun was missing) with a 4-cell matrix that exercises

ioredis because bunx vitest routes through the vitest shebang under

ci: add cross-runtime × cross-driver test matrixReplaces the single-cell workflow (which effectively only tested Node +

a red test can no longer be published.

runtime × driver cell fails, the registry push is skipped entirely —

trigger added) and publish_package depends on needs: tests. If any

publish.yml now calls test.yml as a reusable workflow (workflow_call

Gate publish on tests

don't want to double-bill wall-clock on every internal PR.

same repo — the push trigger already handled that commit, and we

(fork PRs), but a job-level if: skips it when the PR comes from the

on.pull_request stays in place to cover external contributors

PR.

repos — there's no reason not to catch regressions before opening a

feature branches too. GitHub Actions minutes are free for public

Drop the branches: [main] filter on on.push so the matrix runs on

ci: run tests on every push and gate publish on them## Run on every push

  • REDIS_URL=redis://localhost:6399 npx vitest run → 300/300 pass

  • bunx pkgroll → clean build

  • bunx biome check . → 85 files, 0 errors

Verified

the justification rather than rewriting into runtime-assert bloat.

// biome-ignore lint/style/noNonNullAssertion: … comments carrying

calls provably return a value. Suppressed with targeted

or allTerminalDone boolean check two statements earlier — the .get()

terminal-node aggregation. Each one is guarded by an allDepsCompleted

two inside advanceWorkflow's dep-resolution branch, one in the

Three noNonNullAssertion reports in src/memory/backend.ts remained:

Manual

of them — pure formatter output.

of FIXABLE no-non-null-assertion diagnostics. No logic changes in any

of format / organizeImports / no-unused-template-literal / a handful

bunx biome check --write + one --unsafe pass fixed 14 files' worth

Autofixed

by taskora/board — committed for distribution, not authored code.

src/board/static is the pre-built React SPA output that gets served

}

"ignore": ["dist", "ui", "documentation", "src/board/static"]

"files": {

Exclude sub-projects from the root biome walk:

biome.json

after merges.

filtered CI never caught because it only ran against the main branch

of accumulated violations. Pre-existing issue that previous branch-

sub-project), both of which have their own linting concerns and dozens

into ui/ (the React SPA sub-project) and documentation/ (VitePress

run of bunx biome check . was red — the root biome config walked

chore: fix biome lint errors surfaced by push-triggered CIEnabling on: push: for the matrix workflow revealed that every recent

docs: fix ai-skills page to reflect universal Agent Skills spec (45+ agents)

chore: require SKILL.md + metadata.json sync in Documentation sync rule

10.97s, no dead links, no broken sidebar).

Docs build verified locally (bun run build in documentation/ —

a credible trust signal without bloating the file.

README was previously a 15-line stub; this gives first-time visitors

  • README.md — short matrix summary + link to the full docs. The

    Testing sidebar.

  • documentation/.vitepress/config.ts — new page added to the

    the library-level testing story.

    the new cross-runtime page so users looking at test helpers also see

  • documentation/testing/index.md — top-of-page tip pointing at

    publish gate, and local reproduction instructions.

    root cause and fix for each, the workflow wiring including the

    provisioning strategy, the five concrete bugs the matrix caught with

    invocations (including the bunx --bun load-bearing detail), Redis

    story: matrix design, what each cell exercises, runtime-specific

  • documentation/testing/cross-runtime.md (new page) — the full

    reproduce any matrix cell locally.

    upstream Bun issue link, and a copy-paste block showing how to

    section right after Quick Start, with the matrix table, totals, the

  • documentation/index.md — new "Battle-Tested Across Runtimes"

Changes

the docs or the npm page.

every cell being green. This should be visible to anyone landing on

live-Redis test runs per push, with the publish workflow gated on

both ioredis and native Bun.RedisClient), and Deno 2.x, totaling 1,200

the full 300-test integration suite against Node 20, Bun 1.3+ (with

CI matrix is a strong signal of library quality — every commit runs

docs: showcase cross-runtime test matrix on landing and READMETaskora is a new project and trust has to be earned. The cross-runtime

9 new unit tests (134 total).

App.implement() follow in subsequent commits.

Commit 1 of the contract phase — foundation only. App.register() and

without polluting the runtime object shape

  • TaskContract phantom types use declare const unique symbol for inference

  • isTaskContract(value) — runtime type guard

    producers (edge, browser) where shipping a schema library is undesirable

  • staticContract<I, O>(config) — type-only contract for bundle-size-sensitive

  • defineTask(config) — runtime schemas via Standard Schema (Zod/Valibot/ArkType)

can import and dispatch contracts without pulling handler code into its bundle.

This is the foundation for splitting producer and consumer services: a producer

name, schemas, and defaults — with zero runtime dependency on App/Worker/Adapter.

feat(contract): add defineTask/staticContract primitives for producer/consumer splitIntroduces TaskContract<I, O> as a pure, serializable declaration of a task's

vitepress-plugin-package-managers (already wired in .vitepress/config.ts)

in documentation/**/*.md -- always use ::: pm-add from

  • CLAUDE.md: add convention banning hand-rolled npm/pnpm/bun/yarn code-groups

  • Bump skill version 0.1.0 -> 0.2.0 in metadata.json + SKILL.md frontmatter

    REST endpoint table, field redaction details, production checklist

    modes with inline comments, feature bullets, throughput backing, complete

  • SKILL.md Admin dashboard section rewritten: Board interface, four serve

    replaces, Grafana/Datadog pipelines)

  • Cross-link from operations/monitoring -> board (board complements, not

    table, recipes (nginx/sub-route/redaction/IP allowlist), production checklist

    tour (Overview/Task/Job/DAG/Schedules/DLQ/Migrations/SSE), full REST API

    serving (Bun/Deno/Hono/Express via @hono/node-server), all options, feature

docs(board): add dedicated operations/board page and expand skill coverage- New /operations/board page: quick start, Board interface, per-framework

reference it as expected.

into dist/ and both index.html and testing/cross-runtime.html

bun run build in documentation/ — 8.61s clean build. SVG copies

Verified

trimmed to drop the "bugs this matrix caught" phrasing.

for itself via the badge and SVG. The README wording was also

read as self-congratulatory for a young project — the matrix speaks

Removed from both the landing page and the cross-runtime page. It

Cut the "bugs the matrix caught" section

cross-runtime page header as <img src="/testing-matrix.svg" …>.

(#16a34a). Embedded inline in the landing page and in the

the VitePress light/dark toggle; accents use a fixed Tailwind-green

runs per pushfooter. Themed withcurrentColor` so text adapts to

and 300 / 300 per cell, plus a `1,200 live-Redis integration test

grid showing the four runtime × driver cells with green-check icons

New documentation/public/testing-matrix.svg — hand-authored 2x2

2. Custom matrix SVG

page. Auto-updating, links to the workflow run list, zero maintenance.

landing "Battle-Tested" section, and the top of the cross-runtime

Added actions/workflows/test.yml/badge.svg to the README, the

1. Live GitHub Actions status badge

docs: add CI status badge and custom matrix SVG, drop the bugs sectionTwo visual layers to give the cross-runtime story a credible anchor:

leftover references to the removed public/ file.

SVG with var(--vp-c-*) attributes intact, and there are no

index.html and testing/cross-runtime.html contains the inlined

bun run build — 9.79s clean. The rendered HTML for both

Verified

references it anymore.

  • Deleted documentation/public/testing-matrix.svg — nothing

    with <TestingMatrix />.

  • Replaced the <img> tags in index.md and testing/cross-runtime.md

    visualizers (HeroBadges, JobLifecycleVisualizer, etc.).

  • Registered globally in theme/index.ts next to the other

Wiring

OS dark, manual light / manual dark).

The matrix now renders correctly in every combination (OS light /

manual theme toggle — not just the OS prefers-color-scheme setting.

between the light and dark palettes, and they also respect the

These are all VitePress default theme variables that swap values

var(--vp-c-green-soft) — check icon background tint

var(--vp-c-green-1) — check icon + ring + count

var(--vp-c-bg-soft) — cell fill

var(--vp-c-border) — cell borders

var(--vp-c-text-3) — muted driver subtitles

var(--vp-c-text-1) — headline + runtime labels + footer

DOM, it reads VitePress's theme CSS variables directly:

into the component template. Because the SVG is now part of the page

Moved the SVG into .vitepress/components/TestingMatrix.vue, inlined

The fix

the whole graphic was unreadable in VitePress's dark theme.

parent page, so currentColor fell back to the default (black) and

<img> is a sandboxed document — it does not inherit CSS from the

public/testing-matrix.svg referenced via <img>. SVG loaded through

The previous commit shipped the matrix as a standalone

fix(docs): inline TestingMatrix as Vue component so dark theme works## The bug

var(--vp-c-*) theme variables from the previous fix commit).

renders correctly in both light and dark themes (using VitePress

bun run build in documentation/ — 6.66s clean. SVG component

Verified

Removed "(LTS)" from every mention of Node 20 — it is no longer LTS.

  • README.md table — Node 24 + Node 20 rows, "1,500" total.

  • documentation/testing/index.md tip — matrix summary + "1,500".

    the Redis provisioning paragraph.

    and the explicit cell names, "four runtimes" → "five cells" in

    workflow YAML snippet updated to show the nodeVersion dimension

    rows with Node 24 first and Node 20 second (marked maintenance),

  • documentation/testing/cross-runtime.md — matrix table grows to 5

  • documentation/index.md Battle-Tested section — "1,200" → "1,500".

    test runs per push" (5 cells × 300).

    300 per version"; footer bumped to "1,500 live-Redis integration

  • TestingMatrix.vue — cell 1 reads "Node 24 · Node 20" with "300 /

Doc updates across the board

invocation on the same version as the primary baseline.

runtime-agnostic so this is cosmetic, but it keeps every Node

The lint-build job also bumps to Node 24 — biome + pkgroll are

actions/setup-node@v4 version without a forest of if: steps.

Added a nodeVersion matrix dimension so each cell can set its own

  • Deno + ioredis

  • Bun + Bun.RedisClient

  • Bun + ioredis

  • Node 20 + ioredis (kept — maintenance coverage until Apr 2026)

  • Node 24 + ioredis (new — current Active LTS baseline)

Matrix grows from 4 to 5 cells:

library exclusively against a dying version was a bad signal.

April 2026. Labeling Node 20 as "LTS" was wrong, and testing a new

LTS since then, and Node 20 drops into end-of-life maintenance in

Node 20's LTS window ended in October 2025 — Node 24 has been Active

Node 20 → Node 20 + Node 24 in CI matrix

stay consistent with the six existing cards.

convention also documents the style template so future additions

card, and aim for a grid-friendly count (3, 6, or 9). The

ask the user before dropping or adding a landing-page hero feature

New rule added to CLAUDE.md in the Conventions section: always

orphan row the 7-card layout was producing.

goes 7 → 6, giving a clean 3×2 grid on desktop instead of the

the guide section but no longer headlines the home page. Card count

queues and "Koa-Style" is inside-baseball branding — it survives in

after being asked). Middleware is table-stakes for modern task

Koa-Style Middleware dropped from the hero array (per user decision

runtime, runner.run() vs runner.execute(), etc.).

features (retryOn / noRetryOn, Pause, resume, trigger, list at

it compiles, it works" are gone — each card now pitches actual

two short punchy declarations. Abstract value-prop clichés like "If

followed by a comma-separated list of real capabilities, then one or

Admin UI" template: concrete anchor (module name or primary concept)

All six remaining feature cards rewritten in the "Batteries-Included

Landing hero cards — style + count fix

docs + ci: style pass on hero cards, drop Koa middleware, add Node 24Three threads landed together, touching the same surface:

Commit 2 of the contract phase. App.implement() follows in Commit 3.

(separate app instance) executes, producer receives result + event

  • end-to-end: producer dispatches + subscribes to completed, worker

  • producer-only app starts and closes cleanly with no handlers

  • register() wraps an existing app.task()-declared task

  • register() is idempotent for the same contract

  • dispatch via BoundTask writes to Redis and shows up in inspector

5 new integration tests (180 integration total):

safety net in case a worker is somehow started against them directly.

  • Safety handler: contract-only registrations get a throwing handler as a

    without conflict — both are first-class styles.

    lets inline and contract-based declarations coexist on the same name

    existing Task is wrapped as-is and its handler is left untouched. This

    name already exists (from app.task() or a prior register call), the

  • App.register(contract): idempotent by task name. If a task with the same

    by implement() in Commit 3.

    against the underlying Task. Returned by register(); will also be returned

  • BoundTask<TInput, TOutput>: thin wrapper exposing dispatch/dispatchMany/on

    start cleanly with no runtime overhead.

    App.start() skips the Worker loop when false, so producer-only processes

  • Task.hasHandler: boolean flag (default true). Set to false by register().

completed/failed events from a worker running in a separate process.

work as normal — a producer can await handle.result and receive

is skipped for contract-only tasks, but event subscription and dispatch

and dispatch jobs without ever implementing a handler locally. Worker loop

feat(contract): add App.register() + BoundTask for producer-only processesEnables the producer/consumer split: a process can register a task contract

Build verified: bun run build 8.23s, no dead links.

addition to a marketing list.

The hero grid is now explicitly framed as a navigation index in

  1. linkText uses concise active-voice CTAs, never generic ones.

    deepest-dive docs page for that feature.

  2. Every card MUST have link: and linkText: pointing at the

Extended the landing-page hero feature rule with two new clauses:

CLAUDE.md

tells the reader nothing about what they'll find.

retries" — rather than generic "Learn more" / "Read more" which

CTA text is active-voice and specific — "Define a task", "Configure

Batteries-Included UI → /operations/board "Explore the board" (unchanged)

First-Class Testing → /testing/ "Test your tasks"

Flow Control → /features/flow-control "Shape the flow"

Built-in Scheduling → /features/scheduling "Schedule tasks"

Retry & Backoff → /features/retry-backoff "Configure retries"

Type-Safe End-to-End → /guide/tasks "Define a task"

lives.

forcing users to hunt through the sidebar to find where each feature

Admin UI" was clickable — the other five cards were decorative,

deep-dive page for that topic. Previously only "Batteries-Included

linkText: so it doubles as a navigation entry to the canonical

docs(index): add click-to-deep-dive links to every hero feature cardEvery card in the home-page features grid now has link: and

Verified: bun run build 8.01s clean, href resolves in rendered HTML.

keyboard users — otherwise the link would be invisible to tabbing.

  • focus-visible gets a 2px brand-colored outline offset by 4px for

    anchor styling so the matrix doesn't pick up blue underlines.

  • text-decoration: none and color: inherit reset the default

    chrome.

    affordance without requiring a "click me" badge or intrusive

    graphic stays visually the same at rest, but the lift surfaces the

  • Hover: subtle translateY(-2px) lift plus brightness(1.05). The

    just announce "link" with no context.

    not just "matrix" — important because screen readers otherwise

  • aria-label on the wrapping anchor describes where the link goes,

    can't hijack window.opener.

    readers don't lose their place in the docs, and the external link

  • target="_blank" rel="noopener noreferrer" — opens in a new tab so

UX details

diagrams.

people who scan text, and the full matrix for people who look at

now has two affordances for the same destination: the badge for

Same target as the status badge already uses, so the whole section

land on the latest CI run two clicks away.

every Test workflow run, sorted newest first. Click the graphic,

github.com/kravetsone/taskora/actions/workflows/test.yml — the list of

docs page is now wrapped in an pointing at

feat(docs): make TestingMatrix clickable — opens GitHub Actions runsThe SVG matrix visualization on the landing page and the cross-runtime

workflow composition (chain/group/chord from contracts).

Commit 3 of the contract phase. Commit 4 adds TaskContract.s() for

  • version override from implement options propagates to dispatched jobs

  • double implement() throws "already implemented"

    BoundTask reference valid

  • implement() after register() upgrades the task and keeps the early

  • object form with middleware works

  • handler + options form applies concurrency (maxConcurrent === 3)

  • bare handler form runs end-to-end

6 new integration tests (186 integration total):

invalid trailing comma inside the import() call.

import type to work around a Biome formatter bug that inserted an

  • types.ts: switched from inline import() expression to top-level

    construction between register() and implement() (avoids copy-paste).

  • App.buildTaskDeps(): tiny helper extracted to share the TaskDeps

    is not a double-implement — it's the intended upgrade path.

  • Double-implement throws a clear error. Implementing after register()

    Documented as "conceptually readonly for external consumers".

    since, migrations, middleware) dropped readonly to enable the merge.

  • Task fields (handler, config, inputSchema, outputSchema, version,

    upgrade — they point to the same Task instance.

    place. Existing BoundTask references from register() stay valid after

    contract-only registration (register() first, implement() later) in

  • Task._mergeImplementation(): internal method that upgrades a

concurrencyLimit, ttl, collect) only live on implement().

Worker-only fields (concurrency, middleware, onCancel, singleton,

stall, version): implement options → contract defaults → app defaults.

Resolution order for fields shared with the contract (retry, timeout,

})

handler: async (items, ctx) => { ... },

collect: { key: "user-emails", delay: "5s" },

taskora.implement(batchEmail, {

// handler is small relative to the config

// 3. Object form — required for collect tasks, preferred when the

)

{ concurrency: 4, middleware: [withTracing()] },

async (data, ctx) => { ... },

processImage,

taskora.implement(

// 2. Handler + worker-side options

taskora.implement(sendEmail, async (data, ctx) => { ... })

// 1. Bare handler — 90% of cases

Three call forms:

(implement) are now fully symmetric primitives.

execution config. Producer dispatch (register) and worker execution

take a contract (defined anywhere) and attach a handler + worker-side

feat(contract): add App.implement() — attach handlers to contracts with 3 overloadsCompletes the contract-first authoring path. With implement() workers can

screenshots later, one view at a time, without rushing a full gallery.

Schedules, DLQ, Migrations) as text-only for now — can add more

Leaving the other six views (Overview, Job detail, Workflow DAG,

"### Task detail" paragraph. Build verified clean in 7.43s.

relative markdown image in operations/board.md right below the

documentation/public/board/task-detail.jpg and referenced via a

63 KB JPG. No conversion to WebP needed at this size. Placed under

for the "taskora/board actually works and looks good" story.

jobs table with error reporting. It's the single highest-signal shot

one frame — sidebar nav, stat cards, throughput, state tabs, and the

Picked the Task detail view because it shows the most surface area in

refused" error message) visible inline.

jobs plus a retry scenario (attempt 2 with a real "SMTP connection

tabs with the current counts, and a job table with several completed

Failed, Completed), a throughput chart with ~1h of live data, state

per-task view for send-email: stat cards (Redis Keys, Memory, Active,

docs(board): add Task detail screenshot under the "What you get" sectionOne real screenshot from the board-demo seeded dashboard, showing the

Build verified: 8.69s clean.

moments.

here from the home page" and "reading about this specific view"

build time. And repeating it gives a visual anchor at both "landed

browser cache hits on the second render, zero extra bandwidth or

free pattern — VitePress serves the same /board/task-detail.jpg, the

Duplicating a single image file across two spots on one page is a

prose explains exactly what's in the frame.

  1. Contextual slot — still under "### Task detail" where the

    alt text so screen readers don't read the same description twice.

    from the "Explore the board" CTA on the home hero grid. Different

    dashboard?". First thing a visitor sees when they land on the page

  2. Hero slot — right after the intro paragraph, before "## Why a

docs(board): also hoist the Task detail screenshot as a page heroThe same screenshot now appears twice on operations/board.md:

documentation/, and the using-taskora skill.

configuration. Commit 6 syncs documentation across docs/,

Commit 4 of the contract phase. Commit 5 adds validateOnDispatch

  • mixed composition: contract-based BoundTask with inline app.task()

  • BoundTask.map() dispatches one contract-defined job per item

    two App instances, worker implements, workflow runs end-to-end

  • producer/consumer split: producer composes with register() across

  • chord() — group of contract tasks feeding a contract callback

  • group() with tuple result from contract-based tasks

  • chain() from BoundTasks created via implement()

6 new integration tests (192 integration total):

await chain(fetchUser.s({ id }), sendEmail.s()).dispatch()

}

sendEmail: app.register(sendEmailContract),

fetchUser: app.register(fetchUserContract),

const { fetchUser, sendEmail } = {

composition can destructure at the top of a module:

dispatchable, Signatures are composable. Users who want one-liner

keeps the model clean: contracts are pure data, BoundTasks are

The explicit path costs one extra line per contract (register()) and

dispatch time — both compromise the "no magic" principle.

would require either global App state or magical auto-registration at

workflow dispatcher can find the adapter. Adding contract-level .s()

register() required. But a Signature needs a Task reference so the

The obvious API would be sendEmailContract.s({ to: ... }) with no

Design note — why no TaskContract.s():

handler code in the producer bundle.

implements the same contracts and executes the resulting jobs. Zero

BoundTasks, and dispatches. A worker process in another deployment

registers contracts, composes chain/group/chord from the returned

Producer/consumer split workflows now work end-to-end: a producer

serializer at dispatch time.

the Task, which is how the workflow dispatcher resolves adapter and

delegate to the underlying Task — Signatures carry a reference back to

tasks compose identically to inline-declared ones. The three primitives

feat(contract): BoundTask.s() / map() / chunk() — workflow composition from contractsExtends BoundTask with the workflow composition surface so contract-based

docs/, documentation/, and the using-taskora skill.

Commit 5 of the contract phase. Commit 6 syncs documentation across

  • schemaless contract with skipValidation is a no-op

  • skipValidation: false is equivalent to default

  • skipValidation: true per-call overrides default-on

  • validateOnDispatch: false globally bypasses validation

  • default-on accepts valid data (smoke)

  • default-on validates and throws ValidationError on bad data

6 new integration tests (198 integration total):

2 and 3, and automatically threads the new field

2 copies of the inline TaskDeps construction that landed in Commits

  • app.task() and register() refactored to use buildTaskDeps() — removes

    in both the regular and the collect paths

  • Task.dispatch() checks deps.validateOnDispatch && !options.skipValidation

  • DispatchOptions.skipValidation (optional) checked per dispatch

  • TaskDeps.validateOnDispatch (required field) threaded to each Task

  • TaskoraOptions.validateOnDispatch (default true) stored on App

Implementation:

  • Workers always validate. The safety net is still there.

    anyway, and forcing them to throw would be surprising.

    staticContract<I, O>() contracts without Zod can't validate

  • Edge runtimes care about bundle size: a producer that ships

    doubles the cost.

    body checked by tRPC/Zod at the router level) — re-validating

  • Producer may have already validated upstream (e.g. incoming HTTP

Rationale for the option:

is unchanged.

Precedence: per-call > global. Default behaviour (validate on dispatch)

sendEmail.dispatch(data, { skipValidation: true })

// Per-call: skip just this one

createTaskora({ adapter, validateOnDispatch: false })

// Global: skip validation for all dispatches in this app

boundary.

always runs before the handler, so job data is still checked at some

either globally or per-call. Worker-side validation is unaffected — it

feat(contract): add validateOnDispatch + skipValidation for producer-side schema checksAdds a knob to disable Standard Schema validation on the producer side,

Commit 6 closes Phase 19.

multi-service dispatch), not a growth trajectory.

are a release valve for concrete pressures (heavy deps, edge runtime,

production teams eventually do". The default is inline tasks — contracts

contracts as optional tooling for a specific problem, not as "what

guide/splitting-services, SKILL.md section) explicitly position

Framing note: all four user-facing docs (guide/contracts,

inventory. Phases line updated: "1–14, 17a, 18, and 19 complete".

  • CLAUDE.md — Phase 19 delivered block added with the full feature

    by worker-side Standard Schema validation + payload versioning).

    and why no Redis contract registry (runtime drift is already covered

    is idempotent and not strict, why staticContract is a distinct primitive,

    auto-binding, why BoundTask.s() and not TaskContract.s(), why register

    for the decisions taken during Phase 19: why explicit register() over

  • docs/API_DESIGN.md — new §19 Contracts covering the design rationale

    abstract updated to mention producer/consumer split via task contracts.

  • documentation/skills/metadata.json — version bumped 0.2.0 -> 0.3.0,

    API surface with short examples. frontmatter version bumped 0.2.0 -> 0.3.0.

    the default, contracts are for specific problems. Covers the full

    right after "Starting workers & shutdown". Same framing: inline is

  • documentation/skills/using-taskora/SKILL.md — new Contracts section

    after "Tasks" so readers encounter them in the natural order.

    new entries: "Contracts" and "Splitting Services", placed right

  • documentation/.vitepress/config.ts — Essentials sidebar gets two

    git submodules, TS path aliases, codegen, and copy-paste.

    explicit anti-patterns section warns off private npm registries,

    Opens with a tip block reinforcing that monolithic is the default;

    two-entrypoints (default), and monorepo with workspace:* protocol.

    two physical layouts for sharing contract files: single-package

  • documentation/guide/splitting-services.md — new page covering the

    composition, and the validateOnDispatch knob.

    staticContract, register, implement (all three overloads), workflow

    service dispatch to a shared worker pool. Covers defineTask,

    contract path: heavy handler deps, edge/browser producer, or multi-

    the default and gives three concrete reasons to escape into the

    "when to reach for contracts" that explicitly names inline tasks as

  • documentation/guide/contracts.md — new user-facing page. Leads with

split shipped across Commits 1–5.

docs(contract): Phase 19 user-facing docs across guide, skill, and design notesThree-way documentation sync for the contract-first producer/consumer

Closing the distance on v0.1.0.

5-cell cross-runtime matrix as evidence, not marketing.

versions may still break API until 1.0". Counts 332 tests and the

  • Frame v0.x honestly: "stable enough to use in production, but minor

    AI Skills guide.

    npx skills add kravetsone/taskora/documentation/skills per the

    taskora` works — the real command is

  • Fix a bogus install command that claimed `npx claude-skill install

  • Add npm version and license badges alongside the existing CI badge.

    docs links, and the AI Skills install command.

    catalog with deep links, package layout, cross-runtime test matrix,

    showing schema inference + retry + typed dispatch/result, full feature

    proper release README: install with any package manager, quick-start

  • Rewrite from "Work in progress — see site for full vision" into a

README.md:

explicit helps tooling).

only dist was listed — npm includes README by default but being

  • Add README.md and LICENSE to the files array explicitly (previously

    (bun, deno, node, ioredis). Better npm search discoverability.

    (inspector, board, admin-dashboard, dlq), and cross-runtime support

    schema stack (standard-schema, zod, valibot, arktype), observability

    rate-limit), composition (chain, group, chord, canvas, workflow, dag),

    primitives (debounce, throttle, deduplicate, collect, ttl, singleton,

  • Expand keywords from 7 to 68 entries covering the flow-control

    accumulate locally before adding this).

    not evict stale hash-named chunks on its own — I saw 76 files

    on a fresh checkout, but this keeps local builds honest (pkgroll does

  • Add prebuild script that cleans dist/ before pkgroll. CI already runs

    renders properly instead of a blank canvas.

  • Add repository, homepage, bugs, author fields so the npmjs.com page

package.json:

chore(release): prepare package.json + README for first publishPre-release polish for v0.1.0. No code changes.

All 332 tests pass (134 unit + 198 integration).

ticks skip exec() entirely. lastIds still advances per entry.

distribute results and fire handlers in stream order. Enrichment-less

queue every enrichment command into a single shared pipeline, then

Rewrote as two-pass batching: parse all entries from an XREAD response,

with COUNT=100 that was up to ~200 sequential round trips per poll tick.

  1. EventReader.poll() awaited one enrich() call per event (1–2 RTTs each);

    LTRIM -10000 -1 in one pipeline. MAX_LOGS_PER_JOB constant with rationale.

    deleted the whole key on job completion. Now pairs every RPUSH with

    balloon a single :logs list to hundreds of MB before retention finally

  2. addLog() did a raw RPUSH with no upper bound — a verbose handler could

    both commands ride one round trip and succeed/fail together.

    TTL-less metric keys on connection hiccups. Moved to a single pipeline so

  3. incrMetric() fire-and-forget chained INCR → EXPIRE via .then(), leaking

perf(redis): collapse metric TTL race, cap :logs lists, batch XREAD enrichmentThree targeted improvements after reviewing src/redis/ against Redis anti-patterns:
Full Changelog: https://github.com/kravetsone/taskora/commits/v0.1.0