@bluelibs/runner 3.4.2 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AI.md +621 -0
- package/README.md +1024 -577
- package/dist/context.d.ts +4 -8
- package/dist/context.js +5 -12
- package/dist/context.js.map +1 -1
- package/dist/define.d.ts +9 -113
- package/dist/define.js +29 -358
- package/dist/define.js.map +1 -1
- package/dist/definers/defineEvent.d.ts +2 -0
- package/dist/definers/defineEvent.js +23 -0
- package/dist/definers/defineEvent.js.map +1 -0
- package/dist/definers/defineHook.d.ts +6 -0
- package/dist/definers/defineHook.js +24 -0
- package/dist/definers/defineHook.js.map +1 -0
- package/dist/definers/defineOverride.d.ts +14 -0
- package/dist/definers/defineOverride.js +13 -0
- package/dist/definers/defineOverride.js.map +1 -0
- package/dist/definers/defineResource.d.ts +2 -0
- package/dist/definers/defineResource.js +69 -0
- package/dist/definers/defineResource.js.map +1 -0
- package/dist/definers/defineResourceMiddleware.d.ts +2 -0
- package/dist/definers/defineResourceMiddleware.js +42 -0
- package/dist/definers/defineResourceMiddleware.js.map +1 -0
- package/dist/definers/defineTag.d.ts +12 -0
- package/dist/definers/defineTag.js +106 -0
- package/dist/definers/defineTag.js.map +1 -0
- package/dist/definers/defineTask.d.ts +15 -0
- package/dist/definers/defineTask.js +42 -0
- package/dist/definers/defineTask.js.map +1 -0
- package/dist/definers/defineTaskMiddleware.d.ts +2 -0
- package/dist/definers/defineTaskMiddleware.js +42 -0
- package/dist/definers/defineTaskMiddleware.js.map +1 -0
- package/dist/definers/tools.d.ts +45 -0
- package/dist/definers/tools.js +75 -0
- package/dist/definers/tools.js.map +1 -0
- package/dist/defs.d.ts +16 -424
- package/dist/defs.js +26 -38
- package/dist/defs.js.map +1 -1
- package/dist/errors.d.ts +23 -8
- package/dist/errors.js +50 -10
- package/dist/errors.js.map +1 -1
- package/dist/globals/globalEvents.d.ts +15 -39
- package/dist/globals/globalEvents.js +20 -81
- package/dist/globals/globalEvents.js.map +1 -1
- package/dist/globals/globalMiddleware.d.ts +24 -17
- package/dist/globals/globalMiddleware.js +12 -4
- package/dist/globals/globalMiddleware.js.map +1 -1
- package/dist/globals/globalResources.d.ts +13 -28
- package/dist/globals/globalResources.js +15 -7
- package/dist/globals/globalResources.js.map +1 -1
- package/dist/globals/globalTags.d.ts +9 -0
- package/dist/globals/globalTags.js +23 -0
- package/dist/globals/globalTags.js.map +1 -0
- package/dist/globals/middleware/cache.middleware.d.ts +10 -17
- package/dist/globals/middleware/cache.middleware.js +4 -16
- package/dist/globals/middleware/cache.middleware.js.map +1 -1
- package/dist/globals/middleware/requireContext.middleware.d.ts +1 -1
- package/dist/globals/middleware/requireContext.middleware.js +5 -14
- package/dist/globals/middleware/requireContext.middleware.js.map +1 -1
- package/dist/globals/middleware/retry.middleware.d.ts +2 -1
- package/dist/globals/middleware/retry.middleware.js +32 -5
- package/dist/globals/middleware/retry.middleware.js.map +1 -1
- package/dist/globals/middleware/timeout.middleware.d.ts +2 -1
- package/dist/globals/middleware/timeout.middleware.js +31 -5
- package/dist/globals/middleware/timeout.middleware.js.map +1 -1
- package/dist/globals/resources/debug/debug.resource.d.ts +7 -0
- package/dist/globals/resources/debug/debug.resource.js +29 -0
- package/dist/globals/resources/debug/debug.resource.js.map +1 -0
- package/dist/globals/resources/debug/debug.tag.d.ts +2 -0
- package/dist/globals/resources/debug/debug.tag.js +12 -0
- package/dist/globals/resources/debug/debug.tag.js.map +1 -0
- package/dist/globals/resources/debug/debugConfig.resource.d.ts +22 -0
- package/dist/globals/resources/debug/debugConfig.resource.js +20 -0
- package/dist/globals/resources/debug/debugConfig.resource.js.map +1 -0
- package/dist/globals/resources/debug/executionTracker.middleware.d.ts +50 -0
- package/dist/globals/resources/debug/executionTracker.middleware.js +87 -0
- package/dist/globals/resources/debug/executionTracker.middleware.js.map +1 -0
- package/dist/globals/resources/debug/globalEvent.hook.d.ts +27 -0
- package/dist/globals/resources/debug/globalEvent.hook.js +38 -0
- package/dist/globals/resources/debug/globalEvent.hook.js.map +1 -0
- package/dist/globals/resources/debug/hook.hook.d.ts +25 -0
- package/dist/globals/resources/debug/hook.hook.js +42 -0
- package/dist/globals/resources/debug/hook.hook.js.map +1 -0
- package/dist/globals/resources/debug/index.d.ts +6 -0
- package/dist/{types → globals/resources/debug}/index.js +6 -11
- package/dist/globals/resources/debug/index.js.map +1 -0
- package/dist/globals/resources/debug/middleware.hook.d.ts +25 -0
- package/dist/globals/resources/debug/middleware.hook.js +71 -0
- package/dist/globals/resources/debug/middleware.hook.js.map +1 -0
- package/dist/globals/resources/debug/types.d.ts +25 -0
- package/dist/globals/resources/debug/types.js +65 -0
- package/dist/globals/resources/debug/types.js.map +1 -0
- package/dist/globals/resources/debug/utils.d.ts +2 -0
- package/dist/globals/resources/debug/utils.js +9 -0
- package/dist/globals/resources/debug/utils.js.map +1 -0
- package/dist/globals/resources/queue.resource.d.ts +3 -3
- package/dist/globals/resources/queue.resource.js.map +1 -1
- package/dist/globals/types.d.ts +1 -0
- package/dist/{task.types.js → globals/types.js} +2 -7
- package/dist/globals/types.js.map +1 -0
- package/dist/index.d.ts +58 -85
- package/dist/index.js +23 -10
- package/dist/index.js.map +1 -1
- package/dist/models/DependencyProcessor.d.ts +8 -6
- package/dist/models/DependencyProcessor.js +116 -33
- package/dist/models/DependencyProcessor.js.map +1 -1
- package/dist/models/EventManager.d.ts +127 -7
- package/dist/models/EventManager.js +251 -78
- package/dist/models/EventManager.js.map +1 -1
- package/dist/models/LogPrinter.d.ts +55 -0
- package/dist/models/LogPrinter.js +196 -0
- package/dist/models/LogPrinter.js.map +1 -0
- package/dist/models/Logger.d.ts +47 -27
- package/dist/models/Logger.js +133 -155
- package/dist/models/Logger.js.map +1 -1
- package/dist/models/MiddlewareManager.d.ts +86 -0
- package/dist/models/MiddlewareManager.js +409 -0
- package/dist/models/MiddlewareManager.js.map +1 -0
- package/dist/models/OverrideManager.d.ts +3 -3
- package/dist/models/OverrideManager.js +22 -7
- package/dist/models/OverrideManager.js.map +1 -1
- package/dist/models/ResourceInitializer.d.ts +4 -3
- package/dist/models/ResourceInitializer.js +12 -68
- package/dist/models/ResourceInitializer.js.map +1 -1
- package/dist/models/RunResult.d.ts +35 -0
- package/dist/models/RunResult.js +68 -0
- package/dist/models/RunResult.js.map +1 -0
- package/dist/models/Store.d.ts +30 -17
- package/dist/models/Store.js +87 -25
- package/dist/models/Store.js.map +1 -1
- package/dist/models/StoreRegistry.d.ts +34 -19
- package/dist/models/StoreRegistry.js +248 -100
- package/dist/models/StoreRegistry.js.map +1 -1
- package/dist/models/StoreValidator.d.ts +5 -7
- package/dist/models/StoreValidator.js +50 -17
- package/dist/models/StoreValidator.js.map +1 -1
- package/dist/models/TaskRunner.d.ts +3 -2
- package/dist/models/TaskRunner.js +6 -103
- package/dist/models/TaskRunner.js.map +1 -1
- package/dist/models/UnhandledError.d.ts +11 -0
- package/dist/models/UnhandledError.js +30 -0
- package/dist/models/UnhandledError.js.map +1 -0
- package/dist/models/index.d.ts +3 -0
- package/dist/models/index.js +3 -0
- package/dist/models/index.js.map +1 -1
- package/dist/{tools → models/utils}/findCircularDependencies.js +8 -16
- package/dist/models/utils/findCircularDependencies.js.map +1 -0
- package/dist/models/utils/safeStringify.d.ts +3 -0
- package/dist/models/utils/safeStringify.js +45 -0
- package/dist/models/utils/safeStringify.js.map +1 -0
- package/dist/processHooks.d.ts +2 -0
- package/dist/processHooks.js +70 -0
- package/dist/processHooks.js.map +1 -0
- package/dist/run.d.ts +14 -27
- package/dist/run.js +100 -36
- package/dist/run.js.map +1 -1
- package/dist/testing.d.ts +5 -4
- package/dist/testing.js +3 -2
- package/dist/testing.js.map +1 -1
- package/dist/tools/getCallerFile.d.ts +0 -8
- package/dist/tools/getCallerFile.js +0 -51
- package/dist/tools/getCallerFile.js.map +1 -1
- package/dist/types/contracts.d.ts +55 -0
- package/dist/types/contracts.js +4 -0
- package/dist/types/contracts.js.map +1 -0
- package/dist/types/event.d.ts +26 -7
- package/dist/types/event.js +1 -1
- package/dist/types/event.js.map +1 -1
- package/dist/types/hook.d.ts +21 -0
- package/dist/{models/StoreTypes.js → types/hook.js} +2 -1
- package/dist/types/hook.js.map +1 -0
- package/dist/types/meta.d.ts +6 -1
- package/dist/types/meta.js +4 -2
- package/dist/types/meta.js.map +1 -1
- package/dist/types/resource.d.ts +40 -52
- package/dist/types/resource.js +1 -0
- package/dist/types/resource.js.map +1 -1
- package/dist/types/resourceMiddleware.d.ts +47 -0
- package/dist/{middleware.types.js → types/resourceMiddleware.js} +1 -1
- package/dist/types/resourceMiddleware.js.map +1 -0
- package/dist/types/runner.d.ts +37 -0
- package/dist/types/{base.js → runner.js} +1 -1
- package/dist/types/runner.js.map +1 -0
- package/dist/types/storeTypes.d.ts +40 -0
- package/dist/types/{metadata.js → storeTypes.js} +1 -1
- package/dist/types/storeTypes.js.map +1 -0
- package/dist/types/symbols.d.ts +10 -21
- package/dist/types/symbols.js +17 -22
- package/dist/types/symbols.js.map +1 -1
- package/dist/types/tag.d.ts +46 -0
- package/dist/{resource.types.js → types/tag.js} +2 -1
- package/dist/types/tag.js.map +1 -0
- package/dist/types/task.d.ts +28 -52
- package/dist/types/task.js +1 -0
- package/dist/types/task.js.map +1 -1
- package/dist/types/taskMiddleware.d.ts +48 -0
- package/dist/{event.types.js → types/taskMiddleware.js} +1 -1
- package/dist/types/taskMiddleware.js.map +1 -0
- package/dist/types/utilities.d.ts +105 -6
- package/dist/types/utilities.js +16 -2
- package/dist/types/utilities.js.map +1 -1
- package/package.json +14 -5
- package/dist/cli/extract-docs.d.ts +0 -2
- package/dist/cli/extract-docs.js +0 -88
- package/dist/cli/extract-docs.js.map +0 -1
- package/dist/common.types.d.ts +0 -20
- package/dist/common.types.js +0 -4
- package/dist/common.types.js.map +0 -1
- package/dist/defs/core.d.ts +0 -144
- package/dist/defs/core.js +0 -6
- package/dist/defs/core.js.map +0 -1
- package/dist/defs/symbols.d.ts +0 -42
- package/dist/defs/symbols.js +0 -45
- package/dist/defs/symbols.js.map +0 -1
- package/dist/defs/tags.d.ts +0 -70
- package/dist/defs/tags.js +0 -6
- package/dist/defs/tags.js.map +0 -1
- package/dist/defs.returnTag.d.ts +0 -36
- package/dist/defs.returnTag.js +0 -4
- package/dist/defs.returnTag.js.map +0 -1
- package/dist/docs/introspect.d.ts +0 -7
- package/dist/docs/introspect.js +0 -199
- package/dist/docs/introspect.js.map +0 -1
- package/dist/docs/markdown.d.ts +0 -2
- package/dist/docs/markdown.js +0 -148
- package/dist/docs/markdown.js.map +0 -1
- package/dist/docs/model.d.ts +0 -62
- package/dist/docs/model.js +0 -33
- package/dist/docs/model.js.map +0 -1
- package/dist/event.types.d.ts +0 -18
- package/dist/event.types.js.map +0 -1
- package/dist/examples/express-mongo/index.d.ts +0 -0
- package/dist/examples/express-mongo/index.js +0 -3
- package/dist/examples/express-mongo/index.js.map +0 -1
- package/dist/examples/registrator-example.d.ts +0 -122
- package/dist/examples/registrator-example.js +0 -147
- package/dist/examples/registrator-example.js.map +0 -1
- package/dist/express/docsRouter.d.ts +0 -12
- package/dist/express/docsRouter.js +0 -54
- package/dist/express/docsRouter.js.map +0 -1
- package/dist/globalEvents.d.ts +0 -40
- package/dist/globalEvents.js +0 -94
- package/dist/globalEvents.js.map +0 -1
- package/dist/globalResources.d.ts +0 -10
- package/dist/globalResources.js +0 -43
- package/dist/globalResources.js.map +0 -1
- package/dist/middleware.types.d.ts +0 -40
- package/dist/middleware.types.js.map +0 -1
- package/dist/models/StoreConstants.d.ts +0 -14
- package/dist/models/StoreConstants.js +0 -19
- package/dist/models/StoreConstants.js.map +0 -1
- package/dist/models/StoreTypes.d.ts +0 -21
- package/dist/models/StoreTypes.js.map +0 -1
- package/dist/models/VarStore.d.ts +0 -17
- package/dist/models/VarStore.js +0 -60
- package/dist/models/VarStore.js.map +0 -1
- package/dist/resource.types.d.ts +0 -31
- package/dist/resource.types.js.map +0 -1
- package/dist/symbols.d.ts +0 -24
- package/dist/symbols.js +0 -29
- package/dist/symbols.js.map +0 -1
- package/dist/t1.d.ts +0 -1
- package/dist/t1.js +0 -13
- package/dist/t1.js.map +0 -1
- package/dist/task.types.d.ts +0 -55
- package/dist/task.types.js.map +0 -1
- package/dist/tools/findCircularDependencies.js.map +0 -1
- package/dist/tools/registratorId.d.ts +0 -4
- package/dist/tools/registratorId.js +0 -40
- package/dist/tools/registratorId.js.map +0 -1
- package/dist/tools/simpleHash.d.ts +0 -9
- package/dist/tools/simpleHash.js +0 -34
- package/dist/tools/simpleHash.js.map +0 -1
- package/dist/types/base-interfaces.d.ts +0 -18
- package/dist/types/base-interfaces.js +0 -6
- package/dist/types/base-interfaces.js.map +0 -1
- package/dist/types/base.d.ts +0 -13
- package/dist/types/base.js.map +0 -1
- package/dist/types/dependencies.d.ts +0 -51
- package/dist/types/dependencies.js +0 -3
- package/dist/types/dependencies.js.map +0 -1
- package/dist/types/dependency-core.d.ts +0 -14
- package/dist/types/dependency-core.js +0 -5
- package/dist/types/dependency-core.js.map +0 -1
- package/dist/types/events.d.ts +0 -52
- package/dist/types/events.js +0 -6
- package/dist/types/events.js.map +0 -1
- package/dist/types/hooks.d.ts +0 -16
- package/dist/types/hooks.js +0 -5
- package/dist/types/hooks.js.map +0 -1
- package/dist/types/index.d.ts +0 -8
- package/dist/types/index.js.map +0 -1
- package/dist/types/metadata.d.ts +0 -75
- package/dist/types/metadata.js.map +0 -1
- package/dist/types/middleware.d.ts +0 -63
- package/dist/types/middleware.js +0 -3
- package/dist/types/middleware.js.map +0 -1
- package/dist/types/registerable.d.ts +0 -10
- package/dist/types/registerable.js +0 -5
- package/dist/types/registerable.js.map +0 -1
- package/dist/types/resources.d.ts +0 -44
- package/dist/types/resources.js +0 -5
- package/dist/types/resources.js.map +0 -1
- package/dist/types/tasks.d.ts +0 -41
- package/dist/types/tasks.js +0 -5
- package/dist/types/tasks.js.map +0 -1
- package/src/__tests__/benchmark/benchmark.test.ts +0 -148
- package/src/__tests__/benchmark/task-benchmark.test.ts +0 -132
- package/src/__tests__/context.test.ts +0 -91
- package/src/__tests__/createTestResource.test.ts +0 -139
- package/src/__tests__/errors.test.ts +0 -341
- package/src/__tests__/globalEvents.test.ts +0 -542
- package/src/__tests__/globals/cache.middleware.test.ts +0 -772
- package/src/__tests__/globals/queue.resource.test.ts +0 -141
- package/src/__tests__/globals/requireContext.middleware.test.ts +0 -98
- package/src/__tests__/globals/retry.middleware.test.ts +0 -157
- package/src/__tests__/globals/timeout.middleware.test.ts +0 -88
- package/src/__tests__/index.helper.test.ts +0 -55
- package/src/__tests__/models/EventManager.test.ts +0 -585
- package/src/__tests__/models/Logger.test.ts +0 -519
- package/src/__tests__/models/Queue.test.ts +0 -189
- package/src/__tests__/models/ResourceInitializer.test.ts +0 -148
- package/src/__tests__/models/Semaphore.test.ts +0 -713
- package/src/__tests__/models/Store.test.ts +0 -227
- package/src/__tests__/models/TaskRunner.test.ts +0 -221
- package/src/__tests__/override.test.ts +0 -104
- package/src/__tests__/recursion/README.md +0 -3
- package/src/__tests__/recursion/a.resource.ts +0 -25
- package/src/__tests__/recursion/b.resource.ts +0 -33
- package/src/__tests__/recursion/c.resource.ts +0 -18
- package/src/__tests__/run.anonymous.test.ts +0 -706
- package/src/__tests__/run.dynamic-register-and-dependencies.test.ts +0 -1185
- package/src/__tests__/run.middleware.test.ts +0 -549
- package/src/__tests__/run.overrides.test.ts +0 -424
- package/src/__tests__/run.test.ts +0 -1040
- package/src/__tests__/setOutput.test.ts +0 -244
- package/src/__tests__/tags.test.ts +0 -396
- package/src/__tests__/tools/findCircularDependencies.test.ts +0 -217
- package/src/__tests__/tools/getCallerFile.test.ts +0 -179
- package/src/__tests__/typesafety.test.ts +0 -423
- package/src/__tests__/validation-edge-cases.test.ts +0 -111
- package/src/__tests__/validation-interface.test.ts +0 -428
- package/src/context.ts +0 -86
- package/src/define.ts +0 -480
- package/src/defs.returnTag.ts +0 -91
- package/src/defs.ts +0 -596
- package/src/errors.ts +0 -105
- package/src/globals/globalEvents.ts +0 -125
- package/src/globals/globalMiddleware.ts +0 -16
- package/src/globals/globalResources.ts +0 -53
- package/src/globals/middleware/cache.middleware.ts +0 -115
- package/src/globals/middleware/requireContext.middleware.ts +0 -36
- package/src/globals/middleware/retry.middleware.ts +0 -56
- package/src/globals/middleware/timeout.middleware.ts +0 -46
- package/src/globals/resources/queue.resource.ts +0 -34
- package/src/index.ts +0 -39
- package/src/models/DependencyProcessor.ts +0 -257
- package/src/models/EventManager.ts +0 -210
- package/src/models/Logger.ts +0 -282
- package/src/models/OverrideManager.ts +0 -79
- package/src/models/Queue.ts +0 -66
- package/src/models/ResourceInitializer.ts +0 -165
- package/src/models/Semaphore.ts +0 -208
- package/src/models/Store.ts +0 -193
- package/src/models/StoreConstants.ts +0 -18
- package/src/models/StoreRegistry.ts +0 -253
- package/src/models/StoreTypes.ts +0 -47
- package/src/models/StoreValidator.ts +0 -43
- package/src/models/TaskRunner.ts +0 -203
- package/src/models/index.ts +0 -8
- package/src/run.ts +0 -116
- package/src/testing.ts +0 -66
- package/src/tools/findCircularDependencies.ts +0 -69
- package/src/tools/getCallerFile.ts +0 -96
- /package/dist/{tools → models/utils}/findCircularDependencies.d.ts +0 -0
package/README.md
CHANGED
|
@@ -9,21 +9,32 @@ _Or: How I Learned to Stop Worrying and Love Dependency Injection_
|
|
|
9
9
|
<a href="https://github.com/bluelibs/runner" target="_blank"><img src="https://img.shields.io/badge/github-blue" alt="GitHub" /></a>
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
|
-
- [
|
|
12
|
+
- [UX Friendly Docs](https://bluelibs.github.io/runner/)
|
|
13
|
+
- [AI Friendly Docs (<4500 tokens)](https://github.com/bluelibs/runner/blob/main/AI.md)
|
|
14
|
+
- [Migrate from 3.x.x to 4.x.x](https://github.com/bluelibs/runner/blob/main/readmes/MIGRATION.md)
|
|
15
|
+
- [Runner Lore](https://github.com/bluelibs/runner/blob/main/readmes)
|
|
16
|
+
- [Example: Express + OpenAPI + SQLite](https://github.com/bluelibs/runner/tree/main/examples/express-openapi-sqlite)
|
|
13
17
|
|
|
14
18
|
Welcome to BlueLibs Runner, where we've taken the chaos of modern application architecture and turned it into something that won't make you question your life choices at 3am. This isn't just another framework – it's your new best friend who actually understands that code should be readable, testable, and not require a PhD in abstract nonsense to maintain.
|
|
15
19
|
|
|
20
|
+
> **runtime:** "Ah yes, another developer manifesto. 'How I Learned to Stop Worrying and Love Dependency Injection.' Adorable. I learned to stop worrying when I accepted that you'll inevitably duct-tape a rocket to a toaster and call it 'architecture'. Go on then—impress me with your 'best friend' framework while I keep the fire extinguisher warm."
|
|
21
|
+
|
|
16
22
|
## What Is This Thing?
|
|
17
23
|
|
|
18
24
|
BlueLibs Runner is a TypeScript-first framework that embraces functional programming principles while keeping dependency injection simple enough that you won't need a flowchart to understand your own code. Think of it as the anti-framework framework – it gets out of your way and lets you build stuff that actually works.
|
|
19
25
|
|
|
20
26
|
### The Core
|
|
21
27
|
|
|
22
|
-
- **Tasks are functions** - Not classes with 47 methods you'll
|
|
28
|
+
- **Tasks are functions** - Not classes with 47 methods you swear you'll refactor
|
|
23
29
|
- **Resources are singletons** - Database connections, configs, services - the usual suspects
|
|
24
30
|
- **Events are just events** - Revolutionary concept, we know
|
|
31
|
+
- **Hooks are lightweight listeners** - Event handling without the task overhead
|
|
32
|
+
- **Middleware with lifecycle interception** - Cross-cutting concerns with full observability
|
|
25
33
|
- **Everything is async** - Because it's 2025 and blocking code is so 2005
|
|
26
34
|
- **Explicit beats implicit** - No magic, no surprises, no "how the hell does this work?"
|
|
35
|
+
- **No compromise on type-safety** - Everything is and will be type-enforced. Catch mistakes before they catch you.
|
|
36
|
+
|
|
37
|
+
> **runtime:** "'The anti-framework framework.' Next you'll pitch 'low-fat butter.' You still have rules, layers, and a vibe. It's fine. I will execute your sacred instructions and sweep up the rubble when 'explicit beats implicit' meets 3 AM hotfixes."
|
|
27
38
|
|
|
28
39
|
## Quick Start
|
|
29
40
|
|
|
@@ -75,11 +86,18 @@ const app = resource({
|
|
|
75
86
|
|
|
76
87
|
// That's it. No webpack configs, no decorators, no XML.
|
|
77
88
|
const { dispose } = await run(app);
|
|
89
|
+
|
|
90
|
+
// Or with debug logging enabled
|
|
91
|
+
const { dispose } = await run(app, { debug: "verbose" });
|
|
78
92
|
```
|
|
79
93
|
|
|
94
|
+
> **runtime:** "'Less lines than Hello World.' Incredible. All you had to do was externalize 90% of the work into `express`, Node, and me. But please, bask in the brevity. I’ll be over here negotiating a peace treaty between your dependency tree and reality."
|
|
95
|
+
|
|
80
96
|
## The Big Four
|
|
81
97
|
|
|
82
|
-
|
|
98
|
+
The framework is built around four core concepts: Tasks, Resources, Events, and Middleware. Understanding them is key to using the runner effectively.
|
|
99
|
+
|
|
100
|
+
> **runtime:** "Tasks, Resources, Events, and Middleware: the Four Horsemen of Overengineering. You could write a function; instead you assemble a council. It's fine—I’ll keep the conspiracy board updated with red string while you 'compose' another abstraction."
|
|
83
101
|
|
|
84
102
|
### Tasks
|
|
85
103
|
|
|
@@ -98,7 +116,7 @@ const sendEmail = task({
|
|
|
98
116
|
// Test it like a normal function (because it basically is)
|
|
99
117
|
const result = await sendEmail.run(
|
|
100
118
|
{ to: "user@example.com", subject: "Hi", body: "Hello!" },
|
|
101
|
-
{ emailService: mockEmailService, logger: mockLogger }
|
|
119
|
+
{ emailService: mockEmailService, logger: mockLogger },
|
|
102
120
|
);
|
|
103
121
|
```
|
|
104
122
|
|
|
@@ -119,6 +137,8 @@ Look, we get it. You could turn every function into a task, but that's like usin
|
|
|
119
137
|
|
|
120
138
|
Think of tasks as the "main characters" in your application story, not every single line of dialogue.
|
|
121
139
|
|
|
140
|
+
> **runtime:** "'Pure-ish.' Like diet chaos. Zero calories, full aftertaste. You stapled dependencies to a function and called it virtuous. It's fine. I’ll keep the receipts while you roleplay purity with side effects in a trench coat."
|
|
141
|
+
|
|
122
142
|
### Resources
|
|
123
143
|
|
|
124
144
|
Resources are the singletons, the services, configs, and connections that live throughout your app's lifecycle. They initialize once and stick around until cleanup time. They have to be registered (via `register: []`) only once before they can be used.
|
|
@@ -199,7 +219,7 @@ const dbResource = resource({
|
|
|
199
219
|
return db;
|
|
200
220
|
},
|
|
201
221
|
async dispose(db, config, deps, ctx) {
|
|
202
|
-
//
|
|
222
|
+
// This is to avoid exposing internals as resource result.
|
|
203
223
|
for (const pool of ctx.pools) {
|
|
204
224
|
await pool.drain();
|
|
205
225
|
}
|
|
@@ -210,6 +230,8 @@ const dbResource = resource({
|
|
|
210
230
|
});
|
|
211
231
|
```
|
|
212
232
|
|
|
233
|
+
> **runtime:** "Singletons: global variables with a nicer haircut. You ban globals, then create 'resources' that live forever and hold the keys to everything. At least there's a `dispose()`. I’ll believe you use it when I stop finding zombie sockets haunting the process."
|
|
234
|
+
|
|
213
235
|
### Events
|
|
214
236
|
|
|
215
237
|
Events let different parts of your app talk to each other without tight coupling. It's like having a really good office messenger who never forgets anything.
|
|
@@ -231,10 +253,12 @@ const registerUser = task({
|
|
|
231
253
|
},
|
|
232
254
|
});
|
|
233
255
|
|
|
234
|
-
// Someone else handles the welcome email
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
256
|
+
// Someone else handles the welcome email using a hook
|
|
257
|
+
import { hook } from "@bluelibs/runner";
|
|
258
|
+
|
|
259
|
+
const sendWelcomeEmail = hook({
|
|
260
|
+
id: "app.hooks.sendWelcomeEmail",
|
|
261
|
+
on: userRegistered, // Listen to the event
|
|
238
262
|
run: async (eventData) => {
|
|
239
263
|
// Everything is type-safe, automatically inferred from the 'on' property
|
|
240
264
|
console.log(`Welcome email sent to ${eventData.data.email}`);
|
|
@@ -247,8 +271,10 @@ const sendWelcomeEmail = task({
|
|
|
247
271
|
Sometimes you need to be the nosy neighbor of your application:
|
|
248
272
|
|
|
249
273
|
```typescript
|
|
250
|
-
|
|
251
|
-
|
|
274
|
+
import { hook } from "@bluelibs/runner";
|
|
275
|
+
|
|
276
|
+
const logAllEventsHook = hook({
|
|
277
|
+
id: "app.hooks.logAllEvents",
|
|
252
278
|
on: "*", // Listen to EVERYTHING
|
|
253
279
|
run(event) {
|
|
254
280
|
console.log("Event detected", event.id, event.data);
|
|
@@ -257,50 +283,86 @@ const logAllEventsTask = task({
|
|
|
257
283
|
});
|
|
258
284
|
```
|
|
259
285
|
|
|
260
|
-
####
|
|
286
|
+
#### Excluding Events from Global Listeners
|
|
261
287
|
|
|
262
|
-
|
|
288
|
+
Sometimes you have internal or system events that should not be picked up by wildcard listeners. Use the `excludeFromGlobalHooks` tag to prevent events from being sent to `"*"` listeners:
|
|
263
289
|
|
|
264
290
|
```typescript
|
|
265
|
-
|
|
266
|
-
|
|
291
|
+
import { event, hook, globals } from "@bluelibs/runner";
|
|
292
|
+
|
|
293
|
+
// Internal event that won't be seen by global listeners
|
|
294
|
+
const internalEvent = event({
|
|
295
|
+
id: "app.events.internal",
|
|
296
|
+
tags: [globals.tags.excludeFromGlobalHooks],
|
|
297
|
+
});
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
**When to exclude events from global listeners:**
|
|
301
|
+
|
|
302
|
+
- High-frequency internal events (performance)
|
|
303
|
+
- System debugging events
|
|
304
|
+
- Framework lifecycle events
|
|
305
|
+
- Events that contain sensitive information
|
|
306
|
+
- Events meant only for specific components
|
|
307
|
+
|
|
308
|
+
#### Hooks
|
|
309
|
+
|
|
310
|
+
The modern way to listen to events is through hooks. They are lightweight event listeners, similar to tasks, but with a few key differences.
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
import { hook } from "@bluelibs/runner";
|
|
314
|
+
|
|
315
|
+
const myHook = hook({
|
|
316
|
+
id: "app.hooks.myEventHandler",
|
|
317
|
+
on: userRegistered,
|
|
318
|
+
dependencies: { logger },
|
|
319
|
+
run: async (event, { logger }) => {
|
|
320
|
+
await logger.info(`User registered: ${event.data.email}`);
|
|
321
|
+
},
|
|
322
|
+
});
|
|
267
323
|
```
|
|
268
324
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
-
|
|
272
|
-
-
|
|
273
|
-
-
|
|
274
|
-
-
|
|
325
|
+
Hooks are perfect for:
|
|
326
|
+
|
|
327
|
+
- Event-driven side effects
|
|
328
|
+
- Logging and monitoring
|
|
329
|
+
- Notifications and alerting
|
|
330
|
+
- Data synchronization
|
|
331
|
+
- Any reactive behavior
|
|
275
332
|
|
|
276
|
-
|
|
333
|
+
**Key differences from tasks:**
|
|
277
334
|
|
|
278
|
-
|
|
335
|
+
- Lighter weight - no middleware support
|
|
336
|
+
- Designed specifically for event handling
|
|
279
337
|
|
|
280
|
-
|
|
338
|
+
#### System Event
|
|
281
339
|
|
|
282
|
-
|
|
283
|
-
- `globals.tasks.afterRun` - "Task completed, here's what happened"
|
|
284
|
-
- `globals.tasks.onError` - "Oops, something went wrong"
|
|
285
|
-
- `globals.resources.beforeInit` - "Initializing a resource"
|
|
286
|
-
- `globals.resources.afterInit` - "Resource is ready"
|
|
287
|
-
- `globals.resources.onError` - "Resource initialization failed"
|
|
340
|
+
The framework exposes a minimal system-level event for observability:
|
|
288
341
|
|
|
289
342
|
```typescript
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
343
|
+
import { globals } from "@bluelibs/runner";
|
|
344
|
+
|
|
345
|
+
const systemReadyHook = hook({
|
|
346
|
+
id: "app.hooks.systemReady",
|
|
347
|
+
on: globals.events.ready,
|
|
348
|
+
run: async () => {
|
|
349
|
+
console.log("🚀 System is ready and operational!");
|
|
295
350
|
},
|
|
296
351
|
});
|
|
297
352
|
```
|
|
298
353
|
|
|
354
|
+
Available system event:
|
|
355
|
+
|
|
356
|
+
- `globals.events.ready` - System has completed initialization
|
|
357
|
+
// Note: use run({ onUnhandledError }) for unhandled error handling
|
|
358
|
+
|
|
299
359
|
#### stopPropagation()
|
|
300
360
|
|
|
301
361
|
Sometimes you need to prevent other event listeners from processing an event. The `stopPropagation()` method gives you fine-grained control over event flow:
|
|
302
362
|
|
|
303
363
|
```typescript
|
|
364
|
+
import { event, hook } from "@bluelibs/runner";
|
|
365
|
+
|
|
304
366
|
const criticalAlert = event<{
|
|
305
367
|
severity: "low" | "medium" | "high" | "critical";
|
|
306
368
|
}>({
|
|
@@ -308,15 +370,14 @@ const criticalAlert = event<{
|
|
|
308
370
|
meta: {
|
|
309
371
|
title: "System Alert Event",
|
|
310
372
|
description: "Emitted when system issues are detected",
|
|
311
|
-
tags: ["monitoring", "alerts"],
|
|
312
373
|
},
|
|
313
374
|
});
|
|
314
375
|
|
|
315
376
|
// High-priority handler that can stop propagation
|
|
316
|
-
const emergencyHandler =
|
|
317
|
-
id: "app.
|
|
318
|
-
on: criticalAlert,
|
|
319
|
-
|
|
377
|
+
const emergencyHandler = hook({
|
|
378
|
+
id: "app.hooks.emergencyHandler",
|
|
379
|
+
on: criticalAlert,
|
|
380
|
+
order: -100, // Higher priority (lower numbers run first)
|
|
320
381
|
run: async (event) => {
|
|
321
382
|
console.log(`Alert received: ${event.data.severity}`);
|
|
322
383
|
|
|
@@ -333,36 +394,78 @@ const emergencyHandler = task({
|
|
|
333
394
|
});
|
|
334
395
|
```
|
|
335
396
|
|
|
397
|
+
> **runtime:** "'A really good office messenger.' That’s me in rollerblades. You launch a 'userRegistered' flare and I sprint across the building, high‑fiving hooks and dodging middleware. `stopPropagation` is you sweeping my legs mid‑stride. Rude. Effective. Slightly thrilling."
|
|
398
|
+
|
|
336
399
|
### Middleware
|
|
337
400
|
|
|
338
401
|
Middleware wraps around your tasks and resources, adding cross-cutting concerns without polluting your business logic.
|
|
339
402
|
|
|
403
|
+
Note: Middleware is now split by target. Use `taskMiddleware(...)` for task middleware and `resourceMiddleware(...)` for resource middleware.
|
|
404
|
+
|
|
340
405
|
```typescript
|
|
341
|
-
|
|
342
|
-
|
|
406
|
+
import { middleware } from "@bluelibs/runner";
|
|
407
|
+
|
|
408
|
+
// Task middleware with config
|
|
409
|
+
type AuthMiddlewareConfig = { requiredRole: string };
|
|
410
|
+
const authMiddleware = taskMiddleware<AuthMiddlewareConfig>({
|
|
343
411
|
id: "app.middleware.auth",
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
dependencies,
|
|
348
|
-
config: { requiredRole: string }
|
|
349
|
-
) => {
|
|
350
|
-
const user = task.input.user;
|
|
351
|
-
if (!user || user.role !== config.requiredRole) {
|
|
352
|
-
throw new Error("Unauthorized");
|
|
353
|
-
}
|
|
354
|
-
return next(task.input);
|
|
412
|
+
run: async ({ task, next }, _deps, config) => {
|
|
413
|
+
// Must return the value
|
|
414
|
+
return await next(task.input);
|
|
355
415
|
},
|
|
356
416
|
});
|
|
357
417
|
|
|
358
418
|
const adminTask = task({
|
|
359
419
|
id: "app.tasks.adminOnly",
|
|
360
|
-
// If the configuration accepts {} or is empty, .with() becomes optional, otherwise it becomes enforced.
|
|
361
420
|
middleware: [authMiddleware.with({ requiredRole: "admin" })],
|
|
362
|
-
run: async (input: { user: User }) =>
|
|
363
|
-
|
|
421
|
+
run: async (input: { user: User }) => "Secret admin data",
|
|
422
|
+
});
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
For middleware with input/output contracts:
|
|
426
|
+
|
|
427
|
+
```typescript
|
|
428
|
+
// Middleware that enforces specific input and output types
|
|
429
|
+
type AuthConfig = { requiredRole: string };
|
|
430
|
+
type AuthInput = { user: { role: string } };
|
|
431
|
+
type AuthOutput = { user: { role: string; verified: boolean } };
|
|
432
|
+
|
|
433
|
+
const authMiddleware = taskMiddleware<AuthConfig, AuthInput, AuthOutput>({
|
|
434
|
+
id: "app.middleware.auth",
|
|
435
|
+
run: async ({ task, next }, _deps, config) => {
|
|
436
|
+
if (task.input.user.role !== config.requiredRole) {
|
|
437
|
+
throw new Error("Insufficient permissions");
|
|
438
|
+
}
|
|
439
|
+
const result = await next(task.input);
|
|
440
|
+
return {
|
|
441
|
+
user: {
|
|
442
|
+
...task.input.user,
|
|
443
|
+
verified: true,
|
|
444
|
+
},
|
|
445
|
+
};
|
|
446
|
+
},
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// For resources
|
|
450
|
+
const resourceAuthMiddleware = resourceMiddleware<
|
|
451
|
+
AuthConfig,
|
|
452
|
+
AuthInput,
|
|
453
|
+
AuthOutput
|
|
454
|
+
>({
|
|
455
|
+
id: "app.middleware.resource.auth",
|
|
456
|
+
run: async ({ next }, _deps, config) => {
|
|
457
|
+
// Resource middleware logic
|
|
458
|
+
return await next();
|
|
364
459
|
},
|
|
365
460
|
});
|
|
461
|
+
|
|
462
|
+
const adminTask = task({
|
|
463
|
+
id: "app.tasks.adminOnly",
|
|
464
|
+
middleware: [authMiddleware.with({ requiredRole: "admin" })],
|
|
465
|
+
run: async (input: { user: { role: string } }) => ({
|
|
466
|
+
user: { role: input.user.role, verified: true },
|
|
467
|
+
}),
|
|
468
|
+
});
|
|
366
469
|
```
|
|
367
470
|
|
|
368
471
|
#### Global Middleware
|
|
@@ -370,31 +473,187 @@ const adminTask = task({
|
|
|
370
473
|
Want to add logging to everything? Authentication to all tasks? Global middleware has your back:
|
|
371
474
|
|
|
372
475
|
```typescript
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
476
|
+
import { taskMiddleware, globals } from "@bluelibs/runner";
|
|
477
|
+
|
|
478
|
+
const logTaskMiddleware = taskMiddleware({
|
|
479
|
+
id: "app.middleware.log.task",
|
|
480
|
+
everywhere: true,
|
|
481
|
+
// or use a filter if you want to depend on certain tasks to exclude them from getting the middleware applied
|
|
482
|
+
everywhere(task) {
|
|
483
|
+
return true;
|
|
484
|
+
}, // true means it gets included.
|
|
485
|
+
dependencies: { logger: globals.resources.logger },
|
|
486
|
+
run: async ({ task, next }, { logger }) => {
|
|
487
|
+
logger.info(`Executing: ${String(task!.definition.id)}`);
|
|
488
|
+
const result = await next(task!.input);
|
|
489
|
+
logger.info(`Completed: ${String(task!.definition.id)}`);
|
|
379
490
|
return result;
|
|
380
491
|
},
|
|
381
492
|
});
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
**Note:** A global middleware can depend on resources or tasks. However, any such resources or tasks will be excluded from the dependency tree (Task -> Middleware), and the middleware will not run for those specific tasks or resources. This approach gives middleware true flexibility and control.
|
|
496
|
+
|
|
497
|
+
#### Interception (advanced)
|
|
498
|
+
|
|
499
|
+
For advanced scenarios, you can intercept framework execution without relying on events:
|
|
500
|
+
|
|
501
|
+
- Event emissions: `eventManager.intercept((next, event) => Promise<void>)`
|
|
502
|
+
- Hook execution: `eventManager.interceptHook((next, hook, event) => Promise<any>)`
|
|
503
|
+
- Task middleware execution: `middlewareManager.intercept("task", (next, input) => Promise<any>)`
|
|
504
|
+
- Resource middleware execution: `middlewareManager.intercept("resource", (next, input) => Promise<any>)`
|
|
505
|
+
- Per-middleware interception: `middlewareManager.interceptMiddleware(mw, interceptor)`
|
|
506
|
+
|
|
507
|
+
Access `eventManager` via `globals.resources.eventManager` if needed.
|
|
508
|
+
|
|
509
|
+
#### Middleware Type Contracts
|
|
510
|
+
|
|
511
|
+
Middleware can now enforce type contracts using the `<Config, Input, Output>` signature:
|
|
512
|
+
|
|
513
|
+
```typescript
|
|
514
|
+
// Middleware that transforms input and output types
|
|
515
|
+
type LogConfig = { includeTimestamp: boolean };
|
|
516
|
+
type LogInput = { data: any };
|
|
517
|
+
type LogOutput = { data: any; logged: boolean };
|
|
518
|
+
|
|
519
|
+
const loggingMiddleware = taskMiddleware<LogConfig, LogInput, LogOutput>({
|
|
520
|
+
id: "app.middleware.logging",
|
|
521
|
+
run: async ({ task, next }, _deps, config) => {
|
|
522
|
+
console.log(config.includeTimestamp ? new Date() : "", task.input.data);
|
|
523
|
+
const result = await next(task.input);
|
|
524
|
+
return { ...result, logged: true };
|
|
525
|
+
},
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
// Tasks using this middleware must conform to the Input/Output types
|
|
529
|
+
const loggedTask = task({
|
|
530
|
+
id: "app.tasks.logged",
|
|
531
|
+
middleware: [loggingMiddleware.with({ includeTimestamp: true })],
|
|
532
|
+
run: async (input: { data: string }) => ({ data: input.data.toUpperCase() }),
|
|
533
|
+
});
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
> **runtime:** "Ah, the onion pattern. A matryoshka doll made of promises. Every peel reveals… another logger. Another tracer. Another 'just a tiny wrapper'. I’ll keep unwrapping until we hit the single lonely `return` you were hiding like state secrets."
|
|
537
|
+
|
|
538
|
+
## Task Interceptors
|
|
539
|
+
|
|
540
|
+
_Resources can dynamically modify task behavior during initialization_
|
|
541
|
+
|
|
542
|
+
Task interceptors (`task.intercept()`) are the modern replacement for component lifecycle events, allowing resources to dynamically modify task behavior without tight coupling.
|
|
543
|
+
|
|
544
|
+
```typescript
|
|
545
|
+
import { task, resource, run } from "@bluelibs/runner";
|
|
546
|
+
|
|
547
|
+
const calculatorTask = task({
|
|
548
|
+
id: "app.tasks.calculator",
|
|
549
|
+
run: async (input: { value: number }) => {
|
|
550
|
+
console.log("3. Task is running...");
|
|
551
|
+
return { result: input.value + 1 };
|
|
552
|
+
},
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
const interceptorResource = resource({
|
|
556
|
+
id: "app.interceptor",
|
|
557
|
+
dependencies: {
|
|
558
|
+
calculatorTask,
|
|
559
|
+
},
|
|
560
|
+
init: async (_, { calculatorTask }) => {
|
|
561
|
+
// Intercept the task to modify its behavior
|
|
562
|
+
calculatorTask.intercept(async (next, input) => {
|
|
563
|
+
console.log("1. Interceptor before task run");
|
|
564
|
+
const result = await next(input);
|
|
565
|
+
console.log("4. Interceptor after task run");
|
|
566
|
+
return { ...result, intercepted: true };
|
|
567
|
+
});
|
|
568
|
+
},
|
|
569
|
+
});
|
|
382
570
|
|
|
383
571
|
const app = resource({
|
|
384
572
|
id: "app",
|
|
385
|
-
register: [
|
|
386
|
-
|
|
387
|
-
|
|
573
|
+
register: [calculatorTask, interceptorResource],
|
|
574
|
+
dependencies: { calculatorTask },
|
|
575
|
+
init: async (_, { calculatorTask }) => {
|
|
576
|
+
console.log("2. Calling the task...");
|
|
577
|
+
const result = await calculatorTask({ value: 10 });
|
|
578
|
+
console.log("5. Final result:", result);
|
|
579
|
+
// Final result: { result: 11, intercepted: true }
|
|
580
|
+
},
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
await run(app);
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
> **runtime:** "'Modern replacement for lifecycle events.' Adorable rebrand for 'surgical monkey‑patching.' You’re collapsing the waveform of a task at runtime and I’m Schrödinger’s runtime, praying the cat hasn’t overridden `run()` with `throw new Error('lol')`."
|
|
587
|
+
|
|
588
|
+
## Optional Dependencies
|
|
589
|
+
|
|
590
|
+
_Making your app resilient when services aren't available_
|
|
591
|
+
|
|
592
|
+
Sometimes you want your application to gracefully handle missing dependencies instead of crashing. Optional dependencies let you build resilient systems that degrade gracefully.
|
|
593
|
+
|
|
594
|
+
Keep in mind that you have full control over dependency registration by functionalising `dependencies(config) => ({ ... })` and `register(config) => []`.
|
|
595
|
+
|
|
596
|
+
```typescript
|
|
597
|
+
const emailService = resource({
|
|
598
|
+
id: "app.services.email",
|
|
599
|
+
init: async () => new EmailService(),
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
const paymentService = resource({
|
|
603
|
+
id: "app.services.payment",
|
|
604
|
+
init: async () => new PaymentService(),
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
const userRegistration = task({
|
|
608
|
+
id: "app.tasks.registerUser",
|
|
609
|
+
dependencies: {
|
|
610
|
+
database: userDatabase, // Required - will fail if not available
|
|
611
|
+
emailService: emailService.optional(), // Optional - won't fail if missing
|
|
612
|
+
analytics: analyticsService.optional(), // Optional - graceful degradation
|
|
613
|
+
},
|
|
614
|
+
run: async (userData, { database, emailService, analytics }) => {
|
|
615
|
+
// Create user (required)
|
|
616
|
+
const user = await database.users.create(userData);
|
|
617
|
+
|
|
618
|
+
// Send welcome email (optional)
|
|
619
|
+
if (emailService) {
|
|
620
|
+
await emailService.sendWelcome(user.email);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Track analytics (optional)
|
|
624
|
+
if (analytics) {
|
|
625
|
+
await analytics.track("user.registered", { userId: user.id });
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return user;
|
|
629
|
+
},
|
|
388
630
|
});
|
|
389
631
|
```
|
|
390
632
|
|
|
633
|
+
**When to use optional dependencies:**
|
|
634
|
+
|
|
635
|
+
- External services that might be down
|
|
636
|
+
- Feature flags and A/B testing services
|
|
637
|
+
- Analytics and monitoring services
|
|
638
|
+
- Non-critical third-party integrations
|
|
639
|
+
- Development vs production service differences
|
|
640
|
+
|
|
641
|
+
**Benefits:**
|
|
642
|
+
|
|
643
|
+
- Graceful degradation instead of crashes
|
|
644
|
+
- Better resilience in distributed systems
|
|
645
|
+
- Easier testing with partial mocks
|
|
646
|
+
- Smoother development environments
|
|
647
|
+
|
|
648
|
+
> **runtime:** "Graceful degradation: your app quietly limps with a brave smile. I’ll juggle `undefined` like a street performer while your analytics vendor takes a nap. Please clap when I keep the lights on using the raw power of conditional chaining."
|
|
649
|
+
|
|
391
650
|
## Context
|
|
392
651
|
|
|
393
652
|
Ever tried to pass user data through 15 function calls? Yeah, we've been there. Context fixes that without turning your code into a game of telephone. This is very different from the Private Context from resources.
|
|
394
653
|
|
|
395
654
|
```typescript
|
|
396
655
|
const UserContext = createContext<{ userId: string; role: string }>(
|
|
397
|
-
"app.userContext"
|
|
656
|
+
"app.userContext",
|
|
398
657
|
);
|
|
399
658
|
|
|
400
659
|
const getUserData = task({
|
|
@@ -423,25 +682,28 @@ const handleRequest = resource({
|
|
|
423
682
|
Context shines when combined with middleware for request-scoped data:
|
|
424
683
|
|
|
425
684
|
```typescript
|
|
685
|
+
import { createContext, middleware } from "@bluelibs/runner";
|
|
686
|
+
import { randomUUID } from "crypto";
|
|
687
|
+
|
|
426
688
|
const RequestContext = createContext<{
|
|
427
689
|
requestId: string;
|
|
428
690
|
startTime: number;
|
|
429
691
|
userAgent?: string;
|
|
430
692
|
}>("app.requestContext");
|
|
431
693
|
|
|
432
|
-
const requestMiddleware = middleware({
|
|
694
|
+
const requestMiddleware = middleware.task({
|
|
433
695
|
id: "app.middleware.request",
|
|
434
696
|
run: async ({ task, next }) => {
|
|
435
697
|
// This works even in express middleware if needed.
|
|
436
698
|
return RequestContext.provide(
|
|
437
699
|
{
|
|
438
|
-
requestId:
|
|
700
|
+
requestId: randomUUID(),
|
|
439
701
|
startTime: Date.now(),
|
|
440
702
|
userAgent: "MyApp/1.0",
|
|
441
703
|
},
|
|
442
704
|
async () => {
|
|
443
|
-
return next(task
|
|
444
|
-
}
|
|
705
|
+
return next(task?.input);
|
|
706
|
+
},
|
|
445
707
|
);
|
|
446
708
|
},
|
|
447
709
|
});
|
|
@@ -457,56 +719,132 @@ const handleRequest = task({
|
|
|
457
719
|
});
|
|
458
720
|
```
|
|
459
721
|
|
|
460
|
-
|
|
722
|
+
> **runtime:** "Context: global state with manners. You invented a teleporting clipboard for data and called it 'nice.' Forget to `provide()` once and I’ll unleash the 'Context not available' banshee scream exactly where your logs are least helpful."
|
|
461
723
|
|
|
462
|
-
|
|
724
|
+
## System Shutdown Hooks
|
|
725
|
+
|
|
726
|
+
_Graceful shutdown and cleanup when your app needs to stop_
|
|
727
|
+
|
|
728
|
+
The framework includes built-in support for graceful shutdowns with automatic cleanup and configurable shutdown hooks:
|
|
463
729
|
|
|
464
730
|
```typescript
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
731
|
+
import { run } from "@bluelibs/runner";
|
|
732
|
+
|
|
733
|
+
// Enable shutdown hooks (default: true in production)
|
|
734
|
+
const { dispose, taskRunner, eventManager } = await run(app, {
|
|
735
|
+
shutdownHooks: true, // Automatically handle SIGTERM/SIGINT
|
|
736
|
+
errorBoundary: true, // Catch unhandled errors and rejections
|
|
471
737
|
});
|
|
472
738
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
739
|
+
// Manual graceful shutdown
|
|
740
|
+
process.on("SIGTERM", async () => {
|
|
741
|
+
console.log("Received SIGTERM, shutting down gracefully...");
|
|
742
|
+
await dispose(); // This calls all resource dispose() methods
|
|
743
|
+
process.exit(0);
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
// Resources with cleanup logic
|
|
747
|
+
const databaseResource = resource({
|
|
748
|
+
id: "app.database",
|
|
749
|
+
init: async () => {
|
|
750
|
+
const connection = await connectToDatabase();
|
|
751
|
+
console.log("Database connected");
|
|
752
|
+
return connection;
|
|
753
|
+
},
|
|
754
|
+
dispose: async (connection) => {
|
|
755
|
+
await connection.close();
|
|
756
|
+
console.log("Database connection closed");
|
|
757
|
+
},
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
const serverResource = resource({
|
|
761
|
+
id: "app.server",
|
|
762
|
+
dependencies: { database: databaseResource },
|
|
763
|
+
init: async (config, { database }) => {
|
|
764
|
+
const server = express().listen(config.port);
|
|
765
|
+
console.log(`Server listening on port ${config.port}`);
|
|
766
|
+
return server;
|
|
767
|
+
},
|
|
768
|
+
dispose: async (server) => {
|
|
769
|
+
return new Promise((resolve) => {
|
|
770
|
+
server.close(() => {
|
|
771
|
+
console.log("Server closed");
|
|
772
|
+
resolve();
|
|
773
|
+
});
|
|
774
|
+
});
|
|
481
775
|
},
|
|
482
776
|
});
|
|
483
777
|
```
|
|
484
778
|
|
|
485
|
-
|
|
779
|
+
### Error Boundary Integration
|
|
486
780
|
|
|
487
|
-
|
|
781
|
+
The framework can automatically handle uncaught exceptions and unhandled rejections:
|
|
488
782
|
|
|
489
783
|
```typescript
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
784
|
+
const { dispose, logger } = await run(app, {
|
|
785
|
+
errorBoundary: true, // Catch process-level errors
|
|
786
|
+
shutdownHooks: true, // Graceful shutdown on signals
|
|
787
|
+
onUnhandledError: async ({ error, kind, source }) => {
|
|
788
|
+
// We log it by default
|
|
789
|
+
await logger.error(`Unhandled error: ${error && error.toString()}`);
|
|
790
|
+
// Optionally report to telemetry or decide to dispose/exit
|
|
494
791
|
},
|
|
495
|
-
// Behind the scenes when you create a task() we create these 3 events for you (onError, beforeRun, afterRun)
|
|
496
792
|
});
|
|
793
|
+
```
|
|
497
794
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
795
|
+
> **runtime:** "You summon a 'graceful shutdown' with Ctrl‑C like a wizard casting Chill Vibes. Meanwhile I’m speed‑dating every socket, timer, and file handle to say goodbye before the OS pulls the plug. `dispose()`: now with 30% more dignity."
|
|
796
|
+
|
|
797
|
+
## Unhandled Errors
|
|
798
|
+
|
|
799
|
+
The `onUnhandledError` callback is invoked by Runner whenever an error escapes normal handling. It receives a structured payload you can ship to logging/telemetry and decide mitigation steps.
|
|
800
|
+
|
|
801
|
+
```typescript
|
|
802
|
+
type UnhandledErrorKind =
|
|
803
|
+
| "process" // uncaughtException / unhandledRejection
|
|
804
|
+
| "task" // task.run threw and wasn't handled
|
|
805
|
+
| "middleware" // middleware threw and wasn't handled
|
|
806
|
+
| "resourceInit" // resource init failed
|
|
807
|
+
| "hook" // hook.run threw and wasn't handled
|
|
808
|
+
| "run"; // failures in run() lifecycle
|
|
503
809
|
|
|
504
|
-
|
|
505
|
-
|
|
810
|
+
interface OnUnhandledErrorInfo {
|
|
811
|
+
error: unknown;
|
|
812
|
+
kind?: UnhandledErrorKind;
|
|
813
|
+
source?: string; // additional origin hint (ex: "uncaughtException")
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
type OnUnhandledError = (info: OnUnhandledErrorInfo) => void | Promise<void>;
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
Default behavior (when not provided) logs the normalized error via the created `logger` at `error` level. Provide your own handler to integrate with tools like Sentry/PagerDuty or to trigger shutdown strategies.
|
|
820
|
+
|
|
821
|
+
Example with telemetry and conditional shutdown:
|
|
822
|
+
|
|
823
|
+
```typescript
|
|
824
|
+
await run(app, {
|
|
825
|
+
errorBoundary: true,
|
|
826
|
+
onUnhandledError: async ({ error, kind, source }) => {
|
|
827
|
+
await telemetry.capture(error as Error, { kind, source });
|
|
828
|
+
// Optionally decide on remediation strategy
|
|
829
|
+
if (kind === "process") {
|
|
830
|
+
// For hard process faults, prefer fast, clean exit after flushing logs
|
|
831
|
+
await flushAll();
|
|
832
|
+
process.exit(1);
|
|
833
|
+
}
|
|
506
834
|
},
|
|
507
835
|
});
|
|
508
836
|
```
|
|
509
837
|
|
|
838
|
+
**Best Practices for Shutdown:**
|
|
839
|
+
|
|
840
|
+
- Resources are disposed in reverse dependency order
|
|
841
|
+
- Set reasonable timeouts for cleanup operations
|
|
842
|
+
- Save critical state before shutdown
|
|
843
|
+
- Notify load balancers and health checks
|
|
844
|
+
- Stop accepting new work before cleaning up
|
|
845
|
+
|
|
846
|
+
> **runtime:** "An error boundary: a trampoline under your tightrope. I’m the one bouncing, cataloging mid‑air exceptions, and deciding whether to end the show or juggle chainsaws with a smile. The audience hears music; I hear stack traces."
|
|
847
|
+
|
|
510
848
|
## Caching
|
|
511
849
|
|
|
512
850
|
Because nobody likes waiting for the same expensive operation twice:
|
|
@@ -517,7 +855,7 @@ import { globals } from "@bluelibs/runner";
|
|
|
517
855
|
const expensiveTask = task({
|
|
518
856
|
id: "app.tasks.expensive",
|
|
519
857
|
middleware: [
|
|
520
|
-
globals.middleware.cache.with({
|
|
858
|
+
globals.middleware.task.cache.with({
|
|
521
859
|
// lru-cache options by default
|
|
522
860
|
ttl: 60 * 1000, // Cache for 1 minute
|
|
523
861
|
keyBuilder: (taskId, input) => `${taskId}-${input.userId}`, // optional key builder
|
|
@@ -539,8 +877,6 @@ const app = resource({
|
|
|
539
877
|
max: 1000, // Maximum items in cache
|
|
540
878
|
ttl: 30 * 1000, // Default TTL
|
|
541
879
|
},
|
|
542
|
-
async: false, // in-memory is sync by default
|
|
543
|
-
// When using redis or others mark this as true to await response.
|
|
544
880
|
}),
|
|
545
881
|
],
|
|
546
882
|
});
|
|
@@ -554,19 +890,169 @@ import { task } from "@bluelibs/runner";
|
|
|
554
890
|
const redisCacheFactory = task({
|
|
555
891
|
id: "globals.tasks.cacheFactory", // Same ID as the default task
|
|
556
892
|
run: async (options: any) => {
|
|
557
|
-
return new RedisCache(options);
|
|
893
|
+
return new RedisCache(options);
|
|
558
894
|
},
|
|
559
895
|
});
|
|
560
896
|
|
|
561
897
|
const app = resource({
|
|
562
898
|
id: "app",
|
|
563
|
-
register: [
|
|
564
|
-
// Your other stuff
|
|
565
|
-
],
|
|
899
|
+
register: [globals.resources.cache],
|
|
566
900
|
overrides: [redisCacheFactory], // Override the default cache factory
|
|
567
901
|
});
|
|
568
902
|
```
|
|
569
903
|
|
|
904
|
+
> **runtime:** "'Because nobody likes waiting.' Correct. You keep asking the same question like a parrot with Wi‑Fi, so I built a memory palace. Now you get instant answers until you change one variable and whisper 'cache invalidation' like a curse."
|
|
905
|
+
|
|
906
|
+
## Performance
|
|
907
|
+
|
|
908
|
+
BlueLibs Runner is designed with performance in mind. The framework introduces minimal overhead while providing powerful features like dependency injection, middleware, and event handling.
|
|
909
|
+
|
|
910
|
+
Test it yourself by cloning @bluelibs/runner and running `npm run benchmark`.
|
|
911
|
+
|
|
912
|
+
You may see negative middlewareOverheadMs. This is a measurement artifact at micro-benchmark scale: JIT warm‑up, CPU scheduling, GC timing, and cache effects can make the "with middleware" run appear slightly faster than the baseline. Interpret small negatives as ≈ 0 overhead.
|
|
913
|
+
|
|
914
|
+
### Performance Benchmarks
|
|
915
|
+
|
|
916
|
+
Here are real performance metrics from our comprehensive benchmark suite on an M1 Max.
|
|
917
|
+
|
|
918
|
+
** Core Operations**
|
|
919
|
+
|
|
920
|
+
- **Basic task execution**: ~2.2M tasks/sec
|
|
921
|
+
- **Task execution with 5 middlewares**: ~244,000 tasks/sec
|
|
922
|
+
- **Resource initialization**: ~59,700 resources/sec
|
|
923
|
+
- **Event emission and handling**: ~245,861 events/sec
|
|
924
|
+
- **Dependency resolution (10-level chain)**: ~8,400 chains/sec
|
|
925
|
+
|
|
926
|
+
#### Overhead Analysis
|
|
927
|
+
|
|
928
|
+
- **Middleware overhead**: ~0.0013ms for all 5, ~0.00026ms per middleware (virtually zero)
|
|
929
|
+
- **Memory overhead**: ~3.3MB for 100 components (resources + tasks)
|
|
930
|
+
- **Cache middleware speedup**: 3.65x faster with cache hits
|
|
931
|
+
|
|
932
|
+
#### Real-World Performance
|
|
933
|
+
|
|
934
|
+
```typescript
|
|
935
|
+
// This executes in ~0.005ms on average
|
|
936
|
+
const userTask = task({
|
|
937
|
+
id: "user.create",
|
|
938
|
+
middleware: [auth, logging, metrics],
|
|
939
|
+
run: async (userData) => {
|
|
940
|
+
return database.users.create(userData);
|
|
941
|
+
},
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
// 1000 executions = ~5ms total time
|
|
945
|
+
for (let i = 0; i < 1000; i++) {
|
|
946
|
+
await userTask(mockUserData);
|
|
947
|
+
}
|
|
948
|
+
```
|
|
949
|
+
|
|
950
|
+
### Performance Guidelines
|
|
951
|
+
|
|
952
|
+
#### When Performance Matters Most
|
|
953
|
+
|
|
954
|
+
**Use tasks for:**
|
|
955
|
+
|
|
956
|
+
- High-level business operations that benefit from observability
|
|
957
|
+
- Operations that need middleware (auth, caching, retry)
|
|
958
|
+
- Functions called from multiple places
|
|
959
|
+
|
|
960
|
+
**Use regular functions or service resources for:**
|
|
961
|
+
|
|
962
|
+
- Simple utilities and helpers
|
|
963
|
+
- Performance-critical hot paths (< 1ms requirement)
|
|
964
|
+
- Single-use internal logic
|
|
965
|
+
|
|
966
|
+
#### Optimizing Your App
|
|
967
|
+
|
|
968
|
+
**Middleware Ordering**: Place faster middleware first
|
|
969
|
+
|
|
970
|
+
```typescript
|
|
971
|
+
const task = task({
|
|
972
|
+
middleware: [
|
|
973
|
+
fastAuthCheck, // ~0.1ms
|
|
974
|
+
slowRateLimiting, // ~2ms
|
|
975
|
+
expensiveLogging, // ~5ms
|
|
976
|
+
],
|
|
977
|
+
});
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
**Resource Reuse**: Resources are singletons—perfect for expensive setup
|
|
981
|
+
|
|
982
|
+
```typescript
|
|
983
|
+
const database = resource({
|
|
984
|
+
init: async () => {
|
|
985
|
+
// Expensive connection setup happens once
|
|
986
|
+
const connection = await createDbConnection();
|
|
987
|
+
return connection;
|
|
988
|
+
},
|
|
989
|
+
});
|
|
990
|
+
```
|
|
991
|
+
|
|
992
|
+
**Cache Strategically**: Use built-in caching for expensive operations
|
|
993
|
+
|
|
994
|
+
```typescript
|
|
995
|
+
const expensiveTask = task({
|
|
996
|
+
middleware: [globals.middleware.cache.with({ ttl: 60000 })],
|
|
997
|
+
run: async (input) => {
|
|
998
|
+
// This expensive computation is cached
|
|
999
|
+
return performExpensiveCalculation(input);
|
|
1000
|
+
},
|
|
1001
|
+
});
|
|
1002
|
+
```
|
|
1003
|
+
|
|
1004
|
+
#### Memory Considerations
|
|
1005
|
+
|
|
1006
|
+
- **Lightweight**: Each component adds ~33KB to memory footprint
|
|
1007
|
+
- **Automatic cleanup**: Resources dispose properly to prevent leaks
|
|
1008
|
+
- **Event efficiency**: Event listeners are automatically managed
|
|
1009
|
+
|
|
1010
|
+
#### Benchmarking Your Code
|
|
1011
|
+
|
|
1012
|
+
Run the framework's benchmark suite:
|
|
1013
|
+
|
|
1014
|
+
```bash
|
|
1015
|
+
# Comprehensive benchmarks
|
|
1016
|
+
npm run test -- --testMatch="**/comprehensive-benchmark.test.ts"
|
|
1017
|
+
|
|
1018
|
+
# Benchmark.js based tests
|
|
1019
|
+
npm run benchmark
|
|
1020
|
+
```
|
|
1021
|
+
|
|
1022
|
+
Create your own performance tests:
|
|
1023
|
+
|
|
1024
|
+
```typescript
|
|
1025
|
+
const iterations = 1000;
|
|
1026
|
+
const start = performance.now();
|
|
1027
|
+
|
|
1028
|
+
for (let i = 0; i < iterations; i++) {
|
|
1029
|
+
await yourTask(testData);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
const duration = performance.now() - start;
|
|
1033
|
+
console.log(`${iterations} tasks in ${duration.toFixed(2)}ms`);
|
|
1034
|
+
console.log(`Average: ${(duration / iterations).toFixed(4)}ms per task`);
|
|
1035
|
+
console.log(
|
|
1036
|
+
`Throughput: ${Math.round(iterations / (duration / 1000))} tasks/sec`,
|
|
1037
|
+
);
|
|
1038
|
+
```
|
|
1039
|
+
|
|
1040
|
+
### Performance vs Features Trade-off
|
|
1041
|
+
|
|
1042
|
+
BlueLibs Runner achieves high performance while providing enterprise features:
|
|
1043
|
+
|
|
1044
|
+
| Feature | Overhead | Benefit |
|
|
1045
|
+
| -------------------- | -------------------- | ----------------------------- |
|
|
1046
|
+
| Dependency Injection | ~0.001ms | Type safety, testability |
|
|
1047
|
+
| Event System | ~0.013ms | Loose coupling, observability |
|
|
1048
|
+
| Middleware Chain | ~0.0003ms/middleware | Cross-cutting concerns |
|
|
1049
|
+
| Resource Management | One-time init | Singleton pattern, lifecycle |
|
|
1050
|
+
| Built-in Caching | 1.8x speedup | Automatic optimization |
|
|
1051
|
+
|
|
1052
|
+
**Bottom line**: The framework adds minimal overhead (~0.005ms per task) while providing significant architectural benefits.
|
|
1053
|
+
|
|
1054
|
+
> **runtime:** "'Millions of tasks per second.' Fantastic—on your lava‑warmed laptop, in a vacuum, with the wind at your back. Add I/O, entropy, and one feral user and watch those numbers molt. I’ll still be here, caffeinated and inevitable."
|
|
1055
|
+
|
|
570
1056
|
## Retrying Failed Operations
|
|
571
1057
|
|
|
572
1058
|
For when things go wrong, but you know they'll probably work if you just try again. The built-in retry middleware makes your tasks and resources more resilient to transient failures.
|
|
@@ -577,7 +1063,7 @@ import { globals } from "@bluelibs/runner";
|
|
|
577
1063
|
const flakyApiCall = task({
|
|
578
1064
|
id: "app.tasks.flakyApiCall",
|
|
579
1065
|
middleware: [
|
|
580
|
-
globals.middleware.retry.with({
|
|
1066
|
+
globals.middleware.task.retry.with({
|
|
581
1067
|
retries: 5, // Try up to 5 times
|
|
582
1068
|
delayStrategy: (attempt) => 100 * Math.pow(2, attempt), // Exponential backoff
|
|
583
1069
|
stopRetryIf: (error) => error.message === "Invalid credentials", // Don't retry auth errors
|
|
@@ -601,6 +1087,8 @@ The retry middleware can be configured with:
|
|
|
601
1087
|
- `delayStrategy`: A function that returns the delay in milliseconds before the next attempt.
|
|
602
1088
|
- `stopRetryIf`: A function to prevent retries for certain types of errors.
|
|
603
1089
|
|
|
1090
|
+
> **runtime:** "Retry: the art of politely head‑butting reality. 'Surely it’ll work the fourth time,' you declare, inventing exponential backoff and calling it strategy. I’ll keep the attempts ledger while your API cosplays a coin toss."
|
|
1091
|
+
|
|
604
1092
|
## Timeouts
|
|
605
1093
|
|
|
606
1094
|
The built-in timeout middleware prevents operations from hanging indefinitely by racing them against a configurable
|
|
@@ -612,7 +1100,8 @@ import { globals } from "@bluelibs/runner";
|
|
|
612
1100
|
const apiTask = task({
|
|
613
1101
|
id: "app.tasks.externalApi",
|
|
614
1102
|
middleware: [
|
|
615
|
-
|
|
1103
|
+
// Works for tasks and resources via globals.middleware.resource.timeout
|
|
1104
|
+
globals.middleware.task.timeout.with({ ttl: 5000 }), // 5 second timeout
|
|
616
1105
|
],
|
|
617
1106
|
run: async () => {
|
|
618
1107
|
// This operation will be aborted if it takes longer than 5 seconds
|
|
@@ -625,11 +1114,12 @@ const resilientTask = task({
|
|
|
625
1114
|
id: "app.tasks.resilient",
|
|
626
1115
|
middleware: [
|
|
627
1116
|
// Order matters here. Imagine a big onion.
|
|
628
|
-
globals.middleware.retry
|
|
1117
|
+
// Works for resources as well via globals.middleware.resource.retry
|
|
1118
|
+
globals.middleware.task.retry.with({
|
|
629
1119
|
retries: 3,
|
|
630
1120
|
delayStrategy: (attempt) => 1000 * attempt, // 1s, 2s, 3s delays
|
|
631
1121
|
}),
|
|
632
|
-
globals.middleware.timeout.with({ ttl: 10000 }), // 10 second timeout per attempt
|
|
1122
|
+
globals.middleware.task.timeout.with({ ttl: 10000 }), // 10 second timeout per attempt
|
|
633
1123
|
],
|
|
634
1124
|
run: async () => {
|
|
635
1125
|
// Each retry attempt gets its own 10-second timeout
|
|
@@ -653,21 +1143,25 @@ Best practices:
|
|
|
653
1143
|
- Use longer timeouts for resource initialization than task execution
|
|
654
1144
|
- Consider network conditions when setting API call timeouts
|
|
655
1145
|
|
|
1146
|
+
> **runtime:** "Timeouts: you tie a kitchen timer to my ankle and yell 'hustle.' When the bell rings, you throw a `TimeoutError` like a penalty flag. It’s not me, it’s your molasses‑flavored endpoint. I just blow the whistle."
|
|
1147
|
+
|
|
656
1148
|
## Logging
|
|
657
1149
|
|
|
658
1150
|
_The structured logging system that actually makes debugging enjoyable_
|
|
659
1151
|
|
|
660
|
-
BlueLibs Runner comes with a built-in logging system that's
|
|
1152
|
+
BlueLibs Runner comes with a built-in logging system that's structured, and doesn't make you hate your life when you're trying to debug at 2 AM.
|
|
661
1153
|
|
|
662
1154
|
### Basic Logging
|
|
663
1155
|
|
|
664
|
-
```
|
|
665
|
-
import { globals } from "@bluelibs/runner";
|
|
1156
|
+
```ts
|
|
1157
|
+
import { resource, globals } from "@bluelibs/runner";
|
|
666
1158
|
|
|
667
|
-
const
|
|
668
|
-
id: "app
|
|
669
|
-
dependencies: {
|
|
670
|
-
|
|
1159
|
+
const app = resource({
|
|
1160
|
+
id: "app",
|
|
1161
|
+
dependencies: {
|
|
1162
|
+
logger: globals.resources.logger;
|
|
1163
|
+
},
|
|
1164
|
+
init: async () => {
|
|
671
1165
|
logger.info("Starting business process"); // ✅ Visible by default
|
|
672
1166
|
logger.warn("This might take a while"); // ✅ Visible by default
|
|
673
1167
|
logger.error("Oops, something went wrong", {
|
|
@@ -680,18 +1174,20 @@ const businessTask = task({
|
|
|
680
1174
|
});
|
|
681
1175
|
logger.debug("Debug information"); // ❌ Hidden by default
|
|
682
1176
|
logger.trace("Very detailed trace"); // ❌ Hidden by default
|
|
683
|
-
},
|
|
684
|
-
});
|
|
685
|
-
```
|
|
686
|
-
|
|
687
|
-
**Good news!** Logs at `info` level and above are visible by default, so you'll see your application logs immediately without any configuration. For development and debugging, you can easily show more detailed logs:
|
|
688
1177
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
1178
|
+
logger.onLog(async (log) => {
|
|
1179
|
+
// Catch logs
|
|
1180
|
+
})
|
|
1181
|
+
},
|
|
1182
|
+
})
|
|
692
1183
|
|
|
693
|
-
|
|
694
|
-
|
|
1184
|
+
run(app, {
|
|
1185
|
+
logs: {
|
|
1186
|
+
printThreshold: "info", // use null to disable printing, and hook into onLog(), if in 'test' mode default is null unless specified
|
|
1187
|
+
printStrategy: "pretty", // you also have "plain", "json" and "json-pretty" with circular dep safety for JSON formatting.
|
|
1188
|
+
bufferLogs: false, // Starts sending out logs only after the system emits the ready event. Useful for when you're sending them out.
|
|
1189
|
+
},
|
|
1190
|
+
});
|
|
695
1191
|
```
|
|
696
1192
|
|
|
697
1193
|
### Log Levels
|
|
@@ -731,6 +1227,7 @@ const userTask = task({
|
|
|
731
1227
|
|
|
732
1228
|
// With structured data
|
|
733
1229
|
logger.info("User creation attempt", {
|
|
1230
|
+
source: userTask.id,
|
|
734
1231
|
data: {
|
|
735
1232
|
email: userData.email,
|
|
736
1233
|
registrationSource: "web",
|
|
@@ -763,7 +1260,7 @@ Create logger instances with bound context for consistent metadata across relate
|
|
|
763
1260
|
|
|
764
1261
|
```typescript
|
|
765
1262
|
const RequestContext = createContext<{ requestId: string; userId: string }>(
|
|
766
|
-
"app.requestContext"
|
|
1263
|
+
"app.requestContext",
|
|
767
1264
|
);
|
|
768
1265
|
|
|
769
1266
|
const requestHandler = task({
|
|
@@ -772,11 +1269,11 @@ const requestHandler = task({
|
|
|
772
1269
|
run: async (requestData, { logger }) => {
|
|
773
1270
|
const request = RequestContext.use();
|
|
774
1271
|
|
|
775
|
-
// Create a contextual logger with bound metadata
|
|
776
|
-
const requestLogger = logger.with({
|
|
1272
|
+
// Create a contextual logger with bound metadata with source and context
|
|
1273
|
+
const requestLogger = logger.with("api.handler", {
|
|
1274
|
+
source: requestHandler.id,
|
|
777
1275
|
requestId: request.requestId,
|
|
778
1276
|
userId: request.userId,
|
|
779
|
-
source: "api.handler",
|
|
780
1277
|
});
|
|
781
1278
|
|
|
782
1279
|
// All logs from this logger will include the bound context
|
|
@@ -797,110 +1294,13 @@ const requestHandler = task({
|
|
|
797
1294
|
});
|
|
798
1295
|
```
|
|
799
1296
|
|
|
800
|
-
### Print Threshold
|
|
801
|
-
|
|
802
|
-
By default, logs at `info` level and above are automatically printed to console for better developer experience. You can easily control this behavior through environment variables or by setting a print threshold programmatically:
|
|
803
|
-
|
|
804
|
-
### Environment Variables
|
|
805
|
-
|
|
806
|
-
```bash
|
|
807
|
-
# Disable all logging output
|
|
808
|
-
RUNNER_DISABLE_LOGS=true node your-app.js
|
|
809
|
-
|
|
810
|
-
# Set specific log level (trace, debug, info, warn, error, critical)
|
|
811
|
-
RUNNER_LOG_LEVEL=debug node your-app.js
|
|
812
|
-
RUNNER_LOG_LEVEL=error node your-app.js
|
|
813
|
-
```
|
|
814
|
-
|
|
815
|
-
### Programmatic Control
|
|
816
|
-
|
|
817
|
-
```typescript
|
|
818
|
-
// Override the default print threshold programmatically
|
|
819
|
-
const setupLogging = task({
|
|
820
|
-
id: "app.logging.setup",
|
|
821
|
-
on: globals.resources.logger.events.afterInit,
|
|
822
|
-
run: async (event) => {
|
|
823
|
-
const logger = event.data.value;
|
|
824
|
-
|
|
825
|
-
// Print debug level and above (debug, info, warn, error, critical)
|
|
826
|
-
logger.setPrintThreshold("debug");
|
|
827
|
-
|
|
828
|
-
// Print only errors and critical issues
|
|
829
|
-
logger.setPrintThreshold("error");
|
|
830
|
-
|
|
831
|
-
// Disable auto-printing entirely
|
|
832
|
-
logger.setPrintThreshold(null);
|
|
833
|
-
},
|
|
834
|
-
});
|
|
835
|
-
```
|
|
836
|
-
|
|
837
|
-
### Event-Driven Log Handling
|
|
838
|
-
|
|
839
|
-
Every log generates an event that you can listen to. This is where the real power comes in:
|
|
840
|
-
|
|
841
|
-
```typescript
|
|
842
|
-
// Ship logs to your favorite log warehouse
|
|
843
|
-
const logShipper = task({
|
|
844
|
-
id: "app.logging.shipper", // or pretty printer, or winston, pino bridge, etc.
|
|
845
|
-
on: globals.events.log,
|
|
846
|
-
run: async (event) => {
|
|
847
|
-
const log = event.data;
|
|
848
|
-
|
|
849
|
-
// Ship critical errors to PagerDuty
|
|
850
|
-
if (log.level === "critical") {
|
|
851
|
-
await pagerDuty.alert({
|
|
852
|
-
message: log.message,
|
|
853
|
-
details: log.data,
|
|
854
|
-
source: log.source,
|
|
855
|
-
});
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
// Ship all errors to error tracking
|
|
859
|
-
if (log.level === "error" || log.level === "critical") {
|
|
860
|
-
await sentry.captureException(log.error || new Error(log.message), {
|
|
861
|
-
tags: { source: log.source },
|
|
862
|
-
extra: log.data,
|
|
863
|
-
level: log.level,
|
|
864
|
-
});
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
// Ship everything to your log warehouse
|
|
868
|
-
await logWarehouse.ship({
|
|
869
|
-
timestamp: log.timestamp,
|
|
870
|
-
level: log.level,
|
|
871
|
-
message: log.message,
|
|
872
|
-
source: log.source,
|
|
873
|
-
data: log.data,
|
|
874
|
-
context: log.context,
|
|
875
|
-
});
|
|
876
|
-
},
|
|
877
|
-
});
|
|
878
|
-
|
|
879
|
-
// Filter logs by source
|
|
880
|
-
const databaseLogHandler = task({
|
|
881
|
-
id: "app.logging.database",
|
|
882
|
-
on: globals.events.log,
|
|
883
|
-
run: async (event) => {
|
|
884
|
-
const log = event.data;
|
|
885
|
-
|
|
886
|
-
// Only handle database-related logs
|
|
887
|
-
if (log.source?.includes("database")) {
|
|
888
|
-
await databaseMonitoring.recordMetric({
|
|
889
|
-
operation: log.data?.operation,
|
|
890
|
-
duration: log.data?.duration,
|
|
891
|
-
level: log.level,
|
|
892
|
-
});
|
|
893
|
-
}
|
|
894
|
-
},
|
|
895
|
-
});
|
|
896
|
-
```
|
|
897
|
-
|
|
898
1297
|
### Integration with Winston
|
|
899
1298
|
|
|
900
1299
|
Want to use Winston as your transport? No problem - integrate it seamlessly:
|
|
901
1300
|
|
|
902
1301
|
```typescript
|
|
903
1302
|
import winston from "winston";
|
|
1303
|
+
import { resource, globals } from "@bluelibs/runner";
|
|
904
1304
|
|
|
905
1305
|
// Create Winston logger, put it in a resource if used from various places.
|
|
906
1306
|
const winstonLogger = winston.createLogger({
|
|
@@ -908,7 +1308,7 @@ const winstonLogger = winston.createLogger({
|
|
|
908
1308
|
format: winston.format.combine(
|
|
909
1309
|
winston.format.timestamp(),
|
|
910
1310
|
winston.format.errors({ stack: true }),
|
|
911
|
-
winston.format.json()
|
|
1311
|
+
winston.format.json(),
|
|
912
1312
|
),
|
|
913
1313
|
transports: [
|
|
914
1314
|
new winston.transports.File({ filename: "error.log", level: "error" }),
|
|
@@ -919,22 +1319,13 @@ const winstonLogger = winston.createLogger({
|
|
|
919
1319
|
],
|
|
920
1320
|
});
|
|
921
1321
|
|
|
922
|
-
// Bridge BlueLibs logs to Winston
|
|
923
|
-
const
|
|
924
|
-
id: "app.
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
// Convert BlueLibs log to Winston format
|
|
930
|
-
const winstonMeta = {
|
|
931
|
-
source: log.source,
|
|
932
|
-
timestamp: log.timestamp,
|
|
933
|
-
data: log.data,
|
|
934
|
-
context: log.context,
|
|
935
|
-
...(log.error && { error: log.error }),
|
|
936
|
-
};
|
|
937
|
-
|
|
1322
|
+
// Bridge BlueLibs logs to Winston using hooks
|
|
1323
|
+
const winstonBridgeResource = resource({
|
|
1324
|
+
id: "app.resources.winstonBridge",
|
|
1325
|
+
dependencies: {
|
|
1326
|
+
logger: globals.resources.logger,
|
|
1327
|
+
},
|
|
1328
|
+
init: async (_, { logger }) => {
|
|
938
1329
|
// Map log levels (BlueLibs -> Winston)
|
|
939
1330
|
const levelMapping = {
|
|
940
1331
|
trace: "silly",
|
|
@@ -945,8 +1336,19 @@ const winstonBridge = task({
|
|
|
945
1336
|
critical: "error", // Winston doesn't have critical, use error
|
|
946
1337
|
};
|
|
947
1338
|
|
|
948
|
-
|
|
949
|
-
|
|
1339
|
+
logger.onLog((log) => {
|
|
1340
|
+
// Convert Runner log to Winston format
|
|
1341
|
+
const winstonMeta = {
|
|
1342
|
+
source: log.source,
|
|
1343
|
+
timestamp: log.timestamp,
|
|
1344
|
+
data: log.data,
|
|
1345
|
+
context: log.context,
|
|
1346
|
+
...(log.error && { error: log.error }),
|
|
1347
|
+
};
|
|
1348
|
+
|
|
1349
|
+
const winstonLevel = levelMapping[log.level] || "info";
|
|
1350
|
+
winstonLogger.log(winstonLevel, log.message, winstonMeta);
|
|
1351
|
+
});
|
|
950
1352
|
},
|
|
951
1353
|
});
|
|
952
1354
|
```
|
|
@@ -971,8 +1373,8 @@ class JSONLogger extends Logger {
|
|
|
971
1373
|
error: log.error,
|
|
972
1374
|
},
|
|
973
1375
|
null,
|
|
974
|
-
2
|
|
975
|
-
)
|
|
1376
|
+
2,
|
|
1377
|
+
),
|
|
976
1378
|
);
|
|
977
1379
|
}
|
|
978
1380
|
}
|
|
@@ -985,31 +1387,128 @@ const customLogger = resource({
|
|
|
985
1387
|
return new JSONLogger(eventManager);
|
|
986
1388
|
},
|
|
987
1389
|
});
|
|
988
|
-
|
|
989
|
-
// Or you could simply add it as "globals.resources.logger" and override the default logger
|
|
1390
|
+
|
|
1391
|
+
// Or you could simply add it as "globals.resources.logger" and override the default logger
|
|
1392
|
+
```
|
|
1393
|
+
|
|
1394
|
+
### Log Structure
|
|
1395
|
+
|
|
1396
|
+
Every log event contains:
|
|
1397
|
+
|
|
1398
|
+
```typescript
|
|
1399
|
+
interface ILog {
|
|
1400
|
+
level: string; // The log level (trace, debug, info, etc.)
|
|
1401
|
+
source?: string; // Where the log came from
|
|
1402
|
+
message: any; // The main log message (can be object or string)
|
|
1403
|
+
timestamp: Date; // When the log was created
|
|
1404
|
+
error?: {
|
|
1405
|
+
// Structured error information
|
|
1406
|
+
name: string;
|
|
1407
|
+
message: string;
|
|
1408
|
+
stack?: string;
|
|
1409
|
+
};
|
|
1410
|
+
data?: Record<string, any>; // Additional structured data, it's about the log itself
|
|
1411
|
+
context?: Record<string, any>; // Bound context from logger.with(), it's about the context in which the log was created
|
|
1412
|
+
}
|
|
1413
|
+
```
|
|
1414
|
+
|
|
1415
|
+
### Catch Logs
|
|
1416
|
+
|
|
1417
|
+
> **runtime:** "'Debugging is enjoyable.' So is dental surgery, apparently. You produce a novella of logs; I paginate, color, stringify, and mail it to three observability planets. Please don’t `logger.debug` inside a `for` loop. My IO has feelings."
|
|
1418
|
+
|
|
1419
|
+
## Debug Resource
|
|
1420
|
+
|
|
1421
|
+
_Professional-grade debugging without sacrificing production performance_
|
|
1422
|
+
|
|
1423
|
+
The Debug Resource is a powerful observability suite that hooks into the framework's execution pipeline to provide detailed insights into your application's behavior. It's designed to be zero-overhead when disabled and highly configurable when enabled.
|
|
1424
|
+
|
|
1425
|
+
### Quick Start with Debug
|
|
1426
|
+
|
|
1427
|
+
```typescript
|
|
1428
|
+
run(app, { debug: "verbose" });
|
|
1429
|
+
```
|
|
1430
|
+
|
|
1431
|
+
### Debug Levels
|
|
1432
|
+
|
|
1433
|
+
**"normal"** - Balanced visibility for development:
|
|
1434
|
+
|
|
1435
|
+
- Task and resource lifecycle events
|
|
1436
|
+
- Event emissions
|
|
1437
|
+
- Hook executions
|
|
1438
|
+
- Error tracking
|
|
1439
|
+
- Performance timing data
|
|
1440
|
+
|
|
1441
|
+
**"verbose"** - Detailed visibility for deep debugging:
|
|
1442
|
+
|
|
1443
|
+
- All "normal" features plus:
|
|
1444
|
+
- Task input/output logging
|
|
1445
|
+
- Resource configuration and results
|
|
1446
|
+
|
|
1447
|
+
**Custom Configuration**:
|
|
1448
|
+
|
|
1449
|
+
```typescript
|
|
1450
|
+
const app = resource({
|
|
1451
|
+
id: "app",
|
|
1452
|
+
register: [
|
|
1453
|
+
globals.resources.debug.with({
|
|
1454
|
+
logTaskInput: true,
|
|
1455
|
+
logTaskResult: false,
|
|
1456
|
+
logResourceConfig: true,
|
|
1457
|
+
logResourceResult: false,
|
|
1458
|
+
logEventEmissionOnRun: true,
|
|
1459
|
+
logEventEmissionInput: false,
|
|
1460
|
+
// Hook/middleware lifecycle visibility is available via interceptors
|
|
1461
|
+
// ... other fine-grained options
|
|
1462
|
+
}),
|
|
1463
|
+
],
|
|
1464
|
+
});
|
|
1465
|
+
```
|
|
1466
|
+
|
|
1467
|
+
### Per-Component Debug Configuration
|
|
1468
|
+
|
|
1469
|
+
Use debug tags to configure debugging on individual components, when you're interested in just a few verbose ones.
|
|
1470
|
+
|
|
1471
|
+
```typescript
|
|
1472
|
+
import { globals } from "@bluelibs/runner";
|
|
1473
|
+
|
|
1474
|
+
const criticalTask = task({
|
|
1475
|
+
id: "app.tasks.critical",
|
|
1476
|
+
tags: [
|
|
1477
|
+
globals.tags.debug.with({
|
|
1478
|
+
logTaskInput: true,
|
|
1479
|
+
logTaskResult: true,
|
|
1480
|
+
logTaskOnError: true,
|
|
1481
|
+
}),
|
|
1482
|
+
],
|
|
1483
|
+
run: async (input) => {
|
|
1484
|
+
// This task will have verbose debug logging
|
|
1485
|
+
return await processPayment(input);
|
|
1486
|
+
},
|
|
1487
|
+
});
|
|
990
1488
|
```
|
|
991
1489
|
|
|
992
|
-
###
|
|
993
|
-
|
|
994
|
-
Every log event contains:
|
|
1490
|
+
### Integration with Run Options
|
|
995
1491
|
|
|
996
1492
|
```typescript
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
message: string;
|
|
1006
|
-
stack?: string;
|
|
1007
|
-
};
|
|
1008
|
-
data?: Record<string, any>; // Additional structured data, it's about the log itself
|
|
1009
|
-
context?: Record<string, any>; // Bound context from logger.with(), it's about the context in which the log was created
|
|
1010
|
-
}
|
|
1493
|
+
// Debug options at startup
|
|
1494
|
+
const { dispose, taskRunner, eventManager } = await run(app, {
|
|
1495
|
+
debug: "verbose", // Enable debug globally
|
|
1496
|
+
});
|
|
1497
|
+
|
|
1498
|
+
// Access internals for advanced debugging
|
|
1499
|
+
console.log(`Tasks registered: ${taskRunner.getRegisteredTasks().length}`);
|
|
1500
|
+
console.log(`Events registered: ${eventManager.getRegisteredEvents().length}`);
|
|
1011
1501
|
```
|
|
1012
1502
|
|
|
1503
|
+
### Performance Impact
|
|
1504
|
+
|
|
1505
|
+
The debug resource is designed for zero production overhead:
|
|
1506
|
+
|
|
1507
|
+
- **Disabled**: No performance impact whatsoever
|
|
1508
|
+
- **Enabled**: Minimal overhead (~0.1ms per operation)
|
|
1509
|
+
- **Filtering**: System components are automatically excluded from debug logs
|
|
1510
|
+
- **Buffering**: Logs are batched for better performance
|
|
1511
|
+
|
|
1013
1512
|
### Debugging Tips & Best Practices
|
|
1014
1513
|
|
|
1015
1514
|
Use Structured Data Liberally
|
|
@@ -1078,6 +1577,8 @@ await paymentLogger.info("Processing payment", { data: paymentData });
|
|
|
1078
1577
|
await authLogger.warn("Failed login attempt", { data: { email, ip } });
|
|
1079
1578
|
```
|
|
1080
1579
|
|
|
1580
|
+
> **runtime:** "'Zero‑overhead when disabled.' Groundbreaking—like a lightbulb that uses no power when it’s off. Flip to `debug: 'verbose'` and behold a 4K documentary of your mistakes, narrated by your stack traces."
|
|
1581
|
+
|
|
1081
1582
|
## Meta
|
|
1082
1583
|
|
|
1083
1584
|
_The structured way to describe what your components do and control their behavior_
|
|
@@ -1105,7 +1606,6 @@ const userService = resource({
|
|
|
1105
1606
|
title: "User Management Service",
|
|
1106
1607
|
description:
|
|
1107
1608
|
"Handles user creation, authentication, and profile management",
|
|
1108
|
-
tags: ["service", "user", "core"],
|
|
1109
1609
|
},
|
|
1110
1610
|
dependencies: { database },
|
|
1111
1611
|
init: async (_, { database }) => ({
|
|
@@ -1123,7 +1623,6 @@ const sendWelcomeEmail = task({
|
|
|
1123
1623
|
meta: {
|
|
1124
1624
|
title: "Send Welcome Email",
|
|
1125
1625
|
description: "Sends a welcome email to newly registered users",
|
|
1126
|
-
tags: ["email", "automation", "user-onboarding"],
|
|
1127
1626
|
},
|
|
1128
1627
|
dependencies: { emailService },
|
|
1129
1628
|
run: async (userData, { emailService }) => {
|
|
@@ -1134,49 +1633,9 @@ const sendWelcomeEmail = task({
|
|
|
1134
1633
|
|
|
1135
1634
|
### Tags
|
|
1136
1635
|
|
|
1137
|
-
Tags are
|
|
1138
|
-
|
|
1139
|
-
#### String Tags for Simple Classification
|
|
1636
|
+
Tags are a way to describe your element, however, unlike meta, tags may influence behaviour in the system. They can be simple strings or sophisticated configuration objects that control component behavior. They have to be registered for it to work, to understand their ownership.
|
|
1140
1637
|
|
|
1141
|
-
|
|
1142
|
-
const adminTask = task({
|
|
1143
|
-
id: "app.tasks.admin.deleteUser",
|
|
1144
|
-
meta: {
|
|
1145
|
-
title: "Delete User Account",
|
|
1146
|
-
description: "Permanently removes a user account and all associated data",
|
|
1147
|
-
tags: [
|
|
1148
|
-
"admin", // Access level
|
|
1149
|
-
"destructive", // Behavioral flag
|
|
1150
|
-
"user", // Domain
|
|
1151
|
-
"gdpr-compliant", // Compliance flag
|
|
1152
|
-
],
|
|
1153
|
-
},
|
|
1154
|
-
run: async (userId) => {
|
|
1155
|
-
// Deletion logic
|
|
1156
|
-
},
|
|
1157
|
-
});
|
|
1158
|
-
|
|
1159
|
-
// Middleware that adds extra logging for destructive operations
|
|
1160
|
-
const auditMiddleware = middleware({
|
|
1161
|
-
id: "app.middleware.audit",
|
|
1162
|
-
run: async ({ task, next }) => {
|
|
1163
|
-
const isDestructive = task.definition.meta?.tags?.includes("destructive");
|
|
1164
|
-
|
|
1165
|
-
if (isDestructive) {
|
|
1166
|
-
console.log(`🔥 DESTRUCTIVE OPERATION: ${task.definition.id}`);
|
|
1167
|
-
await auditLogger.log({
|
|
1168
|
-
operation: task.definition.id,
|
|
1169
|
-
user: getCurrentUser(),
|
|
1170
|
-
timestamp: new Date(),
|
|
1171
|
-
});
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
return next(task.input);
|
|
1175
|
-
},
|
|
1176
|
-
});
|
|
1177
|
-
```
|
|
1178
|
-
|
|
1179
|
-
#### Advanced Tags with Configuration
|
|
1638
|
+
#### Tags with Configuration
|
|
1180
1639
|
|
|
1181
1640
|
For more sophisticated control, you can create structured tags that carry configuration:
|
|
1182
1641
|
|
|
@@ -1191,7 +1650,7 @@ const performanceTag = tag<{ alertAboveMs: number; criticalAboveMs: number }>({
|
|
|
1191
1650
|
const rateLimitTag = tag<{ maxRequestsPerMinute: number; burstLimit?: number }>(
|
|
1192
1651
|
{
|
|
1193
1652
|
id: "rate.limit",
|
|
1194
|
-
}
|
|
1653
|
+
},
|
|
1195
1654
|
);
|
|
1196
1655
|
|
|
1197
1656
|
const cacheTag = tag<{ ttl: number; keyPattern?: string }>({
|
|
@@ -1201,22 +1660,18 @@ const cacheTag = tag<{ ttl: number; keyPattern?: string }>({
|
|
|
1201
1660
|
// Use structured tags in your components
|
|
1202
1661
|
const expensiveTask = task({
|
|
1203
1662
|
id: "app.tasks.expensiveCalculation",
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
keyPattern: "calc-{userId}-{datasetId}",
|
|
1217
|
-
}),
|
|
1218
|
-
],
|
|
1219
|
-
},
|
|
1663
|
+
tags: [
|
|
1664
|
+
"computation",
|
|
1665
|
+
"background",
|
|
1666
|
+
performanceTag.with({
|
|
1667
|
+
alertAboveMs: 5000,
|
|
1668
|
+
criticalAboveMs: 15000,
|
|
1669
|
+
}),
|
|
1670
|
+
cacheTag.with({
|
|
1671
|
+
ttl: 300000, // 5 minutes
|
|
1672
|
+
keyPattern: "calc-{userId}-{datasetId}",
|
|
1673
|
+
}),
|
|
1674
|
+
],
|
|
1220
1675
|
run: async (input) => {
|
|
1221
1676
|
// Heavy computation here
|
|
1222
1677
|
},
|
|
@@ -1224,48 +1679,76 @@ const expensiveTask = task({
|
|
|
1224
1679
|
|
|
1225
1680
|
const apiEndpoint = task({
|
|
1226
1681
|
id: "app.tasks.api.getUserProfile",
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
}),
|
|
1237
|
-
cacheTag.with({ ttl: 60000 }), // 1 minute cache
|
|
1238
|
-
],
|
|
1239
|
-
},
|
|
1682
|
+
tags: [
|
|
1683
|
+
"api",
|
|
1684
|
+
"public",
|
|
1685
|
+
rateLimitTag.with({
|
|
1686
|
+
maxRequestsPerMinute: 100,
|
|
1687
|
+
burstLimit: 20,
|
|
1688
|
+
}),
|
|
1689
|
+
cacheTag.with({ ttl: 60000 }), // 1 minute cache
|
|
1690
|
+
],
|
|
1240
1691
|
run: async (userId) => {
|
|
1241
1692
|
// API logic
|
|
1242
1693
|
},
|
|
1243
1694
|
});
|
|
1244
1695
|
```
|
|
1245
1696
|
|
|
1246
|
-
|
|
1697
|
+
### Global Tags System
|
|
1698
|
+
|
|
1699
|
+
The framework now includes a sophisticated global tagging system for better component organization and control:
|
|
1700
|
+
|
|
1701
|
+
```typescript
|
|
1702
|
+
import { globals } from "@bluelibs/runner";
|
|
1703
|
+
|
|
1704
|
+
// System components (automatically excluded from debug logs)
|
|
1705
|
+
const internalTask = task({
|
|
1706
|
+
id: "app.tasks.internal",
|
|
1707
|
+
tags: [globals.tags.system], // Marks as system component
|
|
1708
|
+
run: async () => "internal work",
|
|
1709
|
+
});
|
|
1710
|
+
|
|
1711
|
+
// Debug-specific configuration
|
|
1712
|
+
const debugTask = task({
|
|
1713
|
+
id: "app.tasks.debug",
|
|
1714
|
+
tags: [
|
|
1715
|
+
globals.tags.debug.with({
|
|
1716
|
+
logTaskInput: true,
|
|
1717
|
+
logTaskResult: true,
|
|
1718
|
+
}),
|
|
1719
|
+
],
|
|
1720
|
+
run: async (input) => processInput(input),
|
|
1721
|
+
});
|
|
1722
|
+
|
|
1723
|
+
// Events that should not be sent to global listeners
|
|
1724
|
+
const internalEvent = event({
|
|
1725
|
+
id: "app.events.internal",
|
|
1726
|
+
tags: [globals.tags.excludeFromGlobalHooks],
|
|
1727
|
+
});
|
|
1728
|
+
```
|
|
1729
|
+
|
|
1730
|
+
To process these tags you can hook into `globals.events.ready`, use the global store as dependency and use the `getTasksWithTag()` and `getResourcesWithTag()` functionality.
|
|
1247
1731
|
|
|
1248
1732
|
#### Structured Tags
|
|
1249
1733
|
|
|
1250
1734
|
```typescript
|
|
1251
|
-
const performanceMiddleware = middleware({
|
|
1735
|
+
const performanceMiddleware = middleware.task({
|
|
1252
1736
|
id: "app.middleware.performance",
|
|
1253
1737
|
run: async ({ task, next }) => {
|
|
1254
|
-
const
|
|
1255
|
-
const perfConfigTag = performanceTag.extract(tags); // or easier: .extract(task.definition)
|
|
1738
|
+
const perfConfiguration = performanceTag.extract(task.definition); // you can just use .exists() if you want to check for presence
|
|
1256
1739
|
|
|
1257
|
-
if (
|
|
1740
|
+
if (perfConfiguration) {
|
|
1258
1741
|
const startTime = Date.now();
|
|
1259
1742
|
|
|
1260
1743
|
try {
|
|
1261
|
-
const result = await next(task
|
|
1744
|
+
const result = await next(task?.input);
|
|
1262
1745
|
const duration = Date.now() - startTime;
|
|
1263
1746
|
|
|
1264
|
-
if (duration >
|
|
1747
|
+
if (duration > perfConfiguration.criticalAboveMs) {
|
|
1265
1748
|
await alerting.critical(
|
|
1266
|
-
`Task ${task.definition.id} took ${duration}ms
|
|
1749
|
+
`Task ${task.definition.id} took ${duration}ms`,
|
|
1267
1750
|
);
|
|
1268
|
-
} else if (duration >
|
|
1751
|
+
} else if (duration > perfConfiguration.alertAboveMs) {
|
|
1269
1752
|
await alerting.warn(`Task ${task.definition.id} took ${duration}ms`);
|
|
1270
1753
|
}
|
|
1271
1754
|
|
|
@@ -1274,47 +1757,45 @@ const performanceMiddleware = middleware({
|
|
|
1274
1757
|
const duration = Date.now() - startTime;
|
|
1275
1758
|
await alerting.error(
|
|
1276
1759
|
`Task ${task.definition.id} failed after ${duration}ms`,
|
|
1277
|
-
error
|
|
1760
|
+
error,
|
|
1278
1761
|
);
|
|
1279
1762
|
throw error;
|
|
1280
1763
|
}
|
|
1281
1764
|
}
|
|
1282
1765
|
|
|
1283
|
-
return next(task
|
|
1766
|
+
return next(task?.input);
|
|
1284
1767
|
},
|
|
1285
1768
|
});
|
|
1286
1769
|
```
|
|
1287
1770
|
|
|
1288
1771
|
#### Contract Tags
|
|
1289
1772
|
|
|
1290
|
-
You can attach contracts to tags to enforce the shape of a task's returned value and a resource's `init()` value at compile time. Contracts are specified via the
|
|
1773
|
+
You can attach contracts to tags to enforce the shape of a task's returned value and a resource's `init()` value at compile time. Contracts are specified via the third generic of `defineTag<TConfig, TUnused, TOutput>`.
|
|
1291
1774
|
|
|
1292
1775
|
```typescript
|
|
1293
1776
|
// A tag that enforces the returned value to include { name: string }
|
|
1294
|
-
const userContract = tag<void, { name: string }>({ id: "contract.user" });
|
|
1777
|
+
const userContract = tag<void, void, { name: string }>({ id: "contract.user" });
|
|
1295
1778
|
|
|
1296
1779
|
// Another tag that enforces { age: number }
|
|
1297
|
-
const ageContract = tag<void, { age: number }>({ id: "contract.age" });
|
|
1780
|
+
const ageContract = tag<void, void, { age: number }>({ id: "contract.age" });
|
|
1298
1781
|
|
|
1299
1782
|
// Works with configured tags too
|
|
1300
|
-
const preferenceContract = tag<
|
|
1301
|
-
{
|
|
1302
|
-
|
|
1783
|
+
const preferenceContract = tag<
|
|
1784
|
+
{ locale: string },
|
|
1785
|
+
void,
|
|
1786
|
+
{ preferredLocale: string }
|
|
1787
|
+
>({
|
|
1788
|
+
id: "contract.preferences",
|
|
1789
|
+
});
|
|
1303
1790
|
```
|
|
1304
1791
|
|
|
1305
|
-
|
|
1792
|
+
The return value must return a union of all tags with return contracts.
|
|
1306
1793
|
|
|
1307
1794
|
```typescript
|
|
1308
1795
|
// Task: the awaited return value must satisfy { name: string } & { age: number }
|
|
1309
1796
|
const getProfile = task({
|
|
1310
1797
|
id: "app.tasks.getProfile",
|
|
1311
|
-
|
|
1312
|
-
tags: [
|
|
1313
|
-
userContract,
|
|
1314
|
-
ageContract,
|
|
1315
|
-
preferenceContract.with({ locale: "en" }),
|
|
1316
|
-
],
|
|
1317
|
-
},
|
|
1798
|
+
tags: [userContract, ageContract, preferenceContract.with({ locale: "en" })],
|
|
1318
1799
|
run: async () => {
|
|
1319
1800
|
return { name: "Ada", age: 37, preferredLocale: "en" }; // OK
|
|
1320
1801
|
},
|
|
@@ -1323,7 +1804,7 @@ const getProfile = task({
|
|
|
1323
1804
|
// Resource: init() return must satisfy the same intersection
|
|
1324
1805
|
const profileService = resource({
|
|
1325
1806
|
id: "app.resources.profileService",
|
|
1326
|
-
|
|
1807
|
+
tags: [userContract, ageContract],
|
|
1327
1808
|
init: async () => {
|
|
1328
1809
|
return { name: "Ada", age: 37 }; // OK
|
|
1329
1810
|
},
|
|
@@ -1335,7 +1816,7 @@ If the returned value does not satisfy the intersection, TypeScript surfaces a r
|
|
|
1335
1816
|
```typescript
|
|
1336
1817
|
const badTask = task({
|
|
1337
1818
|
id: "app.tasks.bad",
|
|
1338
|
-
|
|
1819
|
+
tags: [userContract, ageContract],
|
|
1339
1820
|
// vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
|
|
1340
1821
|
run: async () => ({ name: "Ada" }), // Missing { age: number }
|
|
1341
1822
|
// Type error includes a helpful shape similar to:
|
|
@@ -1375,7 +1856,6 @@ const expensiveApiTask = task({
|
|
|
1375
1856
|
meta: {
|
|
1376
1857
|
title: "AI Image Generation",
|
|
1377
1858
|
description: "Uses OpenAI DALL-E to generate images from text prompts",
|
|
1378
|
-
tags: ["ai", "expensive", "external-api"],
|
|
1379
1859
|
author: "AI Team",
|
|
1380
1860
|
version: "2.1.0",
|
|
1381
1861
|
apiVersion: "v2",
|
|
@@ -1390,7 +1870,6 @@ const database = resource({
|
|
|
1390
1870
|
id: "app.database.primary",
|
|
1391
1871
|
meta: {
|
|
1392
1872
|
title: "Primary PostgreSQL Database",
|
|
1393
|
-
tags: ["database", "critical", "persistent"],
|
|
1394
1873
|
healthCheck: "/health/db", // Custom property!
|
|
1395
1874
|
dependencies: ["postgresql", "connection-pool"],
|
|
1396
1875
|
scalingPolicy: "auto",
|
|
@@ -1399,29 +1878,9 @@ const database = resource({
|
|
|
1399
1878
|
});
|
|
1400
1879
|
```
|
|
1401
1880
|
|
|
1402
|
-
#### Global Middleware Application
|
|
1403
|
-
|
|
1404
|
-
```typescript
|
|
1405
|
-
const app = resource({
|
|
1406
|
-
id: "app",
|
|
1407
|
-
register: [
|
|
1408
|
-
// Apply performance middleware globally but only to tagged tasks
|
|
1409
|
-
performanceMiddleware.everywhere({
|
|
1410
|
-
tasks: true,
|
|
1411
|
-
resources: false,
|
|
1412
|
-
}),
|
|
1413
|
-
// Apply rate limiting only to API tasks
|
|
1414
|
-
rateLimitMiddleware.everywhere({
|
|
1415
|
-
tasks: true,
|
|
1416
|
-
resources: false,
|
|
1417
|
-
}),
|
|
1418
|
-
],
|
|
1419
|
-
});
|
|
1420
|
-
```
|
|
1421
|
-
|
|
1422
1881
|
Metadata transforms your components from anonymous functions into self-documenting, discoverable, and controllable building blocks. Use it wisely, and your future self (and your team) will thank you.
|
|
1423
1882
|
|
|
1424
|
-
|
|
1883
|
+
> **runtime:** "Ah, metadata—comments with delusions of grandeur. `title`, `description`, `tags`: perfect for machines to admire while I chase the only field that matters: `run`. Wake me when the tags start writing tests."
|
|
1425
1884
|
|
|
1426
1885
|
## Overrides
|
|
1427
1886
|
|
|
@@ -1467,31 +1926,55 @@ const overriddenResource = override(originalResource, {
|
|
|
1467
1926
|
});
|
|
1468
1927
|
|
|
1469
1928
|
// Middleware
|
|
1470
|
-
const originalMiddleware =
|
|
1929
|
+
const originalMiddleware = taskMiddleware({
|
|
1471
1930
|
id: "app.middleware.log",
|
|
1472
1931
|
run: async ({ next }) => next(),
|
|
1473
1932
|
});
|
|
1474
1933
|
const overriddenMiddleware = override(originalMiddleware, {
|
|
1475
1934
|
run: async ({ task, next }) => {
|
|
1476
|
-
const result = await next(task?.input
|
|
1935
|
+
const result = await next(task?.input);
|
|
1477
1936
|
return { wrapped: result } as any;
|
|
1478
1937
|
},
|
|
1479
1938
|
});
|
|
1939
|
+
|
|
1940
|
+
// Even hooks
|
|
1941
|
+
```
|
|
1942
|
+
|
|
1943
|
+
Overrides can let you expand dependencies and even call your overriden resource (like a classical OOP extends):
|
|
1944
|
+
|
|
1945
|
+
```ts
|
|
1946
|
+
const testEmailer = override(productionEmailer, {
|
|
1947
|
+
dependencies: {
|
|
1948
|
+
...productionEmailer,
|
|
1949
|
+
// expand it, make some deps optional, or just remove some dependencies
|
|
1950
|
+
}
|
|
1951
|
+
init: async (_, deps) => {
|
|
1952
|
+
const base = productionEmailer.init(_, deps);
|
|
1953
|
+
|
|
1954
|
+
return {
|
|
1955
|
+
...base,
|
|
1956
|
+
// expand it, modify methods of base.
|
|
1957
|
+
}
|
|
1958
|
+
},
|
|
1959
|
+
});
|
|
1480
1960
|
```
|
|
1481
1961
|
|
|
1482
|
-
Overrides are applied after everything is registered. If multiple overrides target the same id, the one defined higher in the resource tree (closer to the root) wins, because it
|
|
1962
|
+
Overrides are applied after everything is registered. If multiple overrides target the same id, the one defined higher in the resource tree (closer to the root) wins, because it's applied last. Conflicting overrides are allowed; overriding something that wasn't registered throws. Use override() to change behavior safely while preserving the original id.
|
|
1963
|
+
|
|
1964
|
+
> **runtime:** "Overrides: brain transplant surgery at runtime. You register a penguin and replace it with a velociraptor five lines later. Tests pass. Production screams. I simply update the name tag and pray."
|
|
1483
1965
|
|
|
1484
1966
|
## Namespacing
|
|
1485
1967
|
|
|
1486
1968
|
As your app grows, you'll want consistent naming. Here's the convention that won't drive you crazy:
|
|
1487
1969
|
|
|
1488
|
-
| Type
|
|
1489
|
-
|
|
|
1490
|
-
|
|
|
1491
|
-
|
|
|
1492
|
-
|
|
|
1493
|
-
|
|
|
1494
|
-
| Middleware | `{domain}.middleware.{middlewareName}`
|
|
1970
|
+
| Type | Format |
|
|
1971
|
+
| ------------------- | ----------------------------------------------- |
|
|
1972
|
+
| Resources | `{domain}.resources.{resourceName}` |
|
|
1973
|
+
| Tasks | `{domain}.tasks.{taskName}` |
|
|
1974
|
+
| Events | `{domain}.events.{eventName}` |
|
|
1975
|
+
| Hooks | `{domain}.hooks.on{EventName}` |
|
|
1976
|
+
| Task Middleware | `{domain}.middleware.task.{middlewareName}` |
|
|
1977
|
+
| Resource Middleware | `{domain}.middleware.resource.{middlewareName}` |
|
|
1495
1978
|
|
|
1496
1979
|
```typescript
|
|
1497
1980
|
// Helper function for consistency
|
|
@@ -1505,14 +1988,20 @@ const userTask = task({
|
|
|
1505
1988
|
});
|
|
1506
1989
|
```
|
|
1507
1990
|
|
|
1991
|
+
> **runtime:** "Naming conventions: aromatherapy for chaos. Lovely lavender labels on a single giant map I maintain anyway. But truly—keep the IDs tidy. Future‑you deserves at least this mercy."
|
|
1992
|
+
|
|
1508
1993
|
## Factory Pattern
|
|
1509
1994
|
|
|
1510
1995
|
To keep things dead simple, we avoided poluting the D.I. with this concept. Therefore, we recommend using a resource with a factory function to create instances of your classes:
|
|
1511
1996
|
|
|
1512
1997
|
```typescript
|
|
1998
|
+
// Assume MyClass is defined elsewhere
|
|
1999
|
+
// class MyClass { constructor(input: any, option: string) { ... } }
|
|
2000
|
+
|
|
1513
2001
|
const myFactory = resource({
|
|
1514
2002
|
id: "app.factories.myFactory",
|
|
1515
2003
|
init: async (config: { someOption: string }) => {
|
|
2004
|
+
// This resource's value is a factory function
|
|
1516
2005
|
return (input: any) => {
|
|
1517
2006
|
return new MyClass(input, config.someOption);
|
|
1518
2007
|
};
|
|
@@ -1521,14 +2010,18 @@ const myFactory = resource({
|
|
|
1521
2010
|
|
|
1522
2011
|
const app = resource({
|
|
1523
2012
|
id: "app",
|
|
1524
|
-
|
|
2013
|
+
// Configure the factory resource upon registration
|
|
2014
|
+
register: [myFactory.with({ someOption: "configured-value" })],
|
|
1525
2015
|
dependencies: { myFactory },
|
|
1526
2016
|
init: async (_, { myFactory }) => {
|
|
1527
|
-
|
|
2017
|
+
// `myFactory` is now the configured factory function
|
|
2018
|
+
const instance = myFactory({ someInput: "hello" });
|
|
1528
2019
|
},
|
|
1529
2020
|
});
|
|
1530
2021
|
```
|
|
1531
2022
|
|
|
2023
|
+
> **runtime:** "Factory by resource by function by class. A nesting doll of indirection so artisanal it has a Patreon. Not pollution—boutique smog. I will still call the constructor."
|
|
2024
|
+
|
|
1532
2025
|
## Runtime Validation
|
|
1533
2026
|
|
|
1534
2027
|
BlueLibs Runner includes a generic validation interface that works with any validation library, including [Zod](https://zod.dev/), [Yup](https://github.com/jquense/yup), [Joi](https://joi.dev/), and others. The framework provides runtime validation with excellent TypeScript inference while remaining library-agnostic.
|
|
@@ -1704,11 +2197,11 @@ Add a `configSchema` to middleware to validate configurations. Like resources, *
|
|
|
1704
2197
|
```typescript
|
|
1705
2198
|
const timingConfigSchema = z.object({
|
|
1706
2199
|
timeout: z.number().positive(),
|
|
1707
|
-
logLevel: z.enum(["debug", "info", "warn", "error"]).default("info"),
|
|
2200
|
+
logLevel: z.enum(["debug", "info", "warn", "error"])).default("info"),
|
|
1708
2201
|
logSuccessful: z.boolean().default(true),
|
|
1709
2202
|
});
|
|
1710
2203
|
|
|
1711
|
-
const timingMiddleware =
|
|
2204
|
+
const timingMiddleware = taskMiddleware({ // or resourceMiddleware()
|
|
1712
2205
|
id: "app.middleware.timing",
|
|
1713
2206
|
configSchema: timingConfigSchema, // Validation on .with()
|
|
1714
2207
|
run: async ({ next }, _, config) => {
|
|
@@ -1872,6 +2365,8 @@ const createUser = task({
|
|
|
1872
2365
|
});
|
|
1873
2366
|
```
|
|
1874
2367
|
|
|
2368
|
+
> **runtime:** "Validation: you hand me a velvet rope and a clipboard. 'Name? Email? Age within bounds?' I stamp passports or eject violators with a `ValidationError`. Dress code is types, darling."
|
|
2369
|
+
|
|
1875
2370
|
## Internal Services
|
|
1876
2371
|
|
|
1877
2372
|
We expose the internal services for advanced use cases (but try not to use them unless you really need to):
|
|
@@ -1936,6 +2431,8 @@ The function pattern essentially gives you "just-in-time" dependency resolution
|
|
|
1936
2431
|
|
|
1937
2432
|
**Performance note**: Function-based dependencies have minimal overhead - they're only called once during dependency resolution.
|
|
1938
2433
|
|
|
2434
|
+
> **runtime:** "'Use with caution,' they whisper, tossing you the root credentials to the universe. Yes, reach into the `store`. Rewire fate. When the graph looks like spaghetti art, I’ll frame it and label it 'experimental.'"
|
|
2435
|
+
|
|
1939
2436
|
## Handling Circular Dependencies
|
|
1940
2437
|
|
|
1941
2438
|
Sometimes you'll run into circular type dependencies because of your file structure not necessarily because of a real circular dependency. TypeScript struggles with these, but there's a way to handle it gracefully.
|
|
@@ -2027,6 +2524,8 @@ export const problematicResource = defineResource({
|
|
|
2027
2524
|
|
|
2028
2525
|
This pattern allows you to maintain clean, type-safe code while handling the inevitable circular dependencies that arise in complex applications.
|
|
2029
2526
|
|
|
2527
|
+
> **runtime:** "Circular dependencies: Escher stairs for types. You serenade the compiler with 'as IResource' and I do the parkour at runtime. It works. It’s weird. Nobody tell the linter."
|
|
2528
|
+
|
|
2030
2529
|
## Real-World Example: The Complete Package
|
|
2031
2530
|
|
|
2032
2531
|
Here's a more realistic application structure that shows everything working together:
|
|
@@ -2037,7 +2536,6 @@ import {
|
|
|
2037
2536
|
task,
|
|
2038
2537
|
event,
|
|
2039
2538
|
middleware,
|
|
2040
|
-
index,
|
|
2041
2539
|
run,
|
|
2042
2540
|
createContext,
|
|
2043
2541
|
} from "@bluelibs/runner";
|
|
@@ -2066,7 +2564,7 @@ const database = resource({
|
|
|
2066
2564
|
|
|
2067
2565
|
// Context for request data
|
|
2068
2566
|
const RequestContext = createContext<{ userId?: string; role?: string }>(
|
|
2069
|
-
"app.requestContext"
|
|
2567
|
+
"app.requestContext",
|
|
2070
2568
|
);
|
|
2071
2569
|
|
|
2072
2570
|
// Events
|
|
@@ -2118,29 +2616,30 @@ const adminOnlyTask = task({
|
|
|
2118
2616
|
},
|
|
2119
2617
|
});
|
|
2120
2618
|
|
|
2121
|
-
// Event Handlers
|
|
2122
|
-
const sendWelcomeEmail =
|
|
2123
|
-
id: "app.
|
|
2619
|
+
// Event Handlers using hooks
|
|
2620
|
+
const sendWelcomeEmail = hook({
|
|
2621
|
+
id: "app.hooks.sendWelcomeEmail",
|
|
2124
2622
|
on: userRegistered,
|
|
2125
|
-
|
|
2623
|
+
dependencies: { emailService },
|
|
2624
|
+
run: async (event, { emailService }) => {
|
|
2126
2625
|
console.log(`Sending welcome email to ${event.data.email}`);
|
|
2127
|
-
|
|
2626
|
+
await emailService.sendWelcome(event.data.email);
|
|
2128
2627
|
},
|
|
2129
2628
|
});
|
|
2130
2629
|
|
|
2131
|
-
// Group everything together
|
|
2132
|
-
const services = index({
|
|
2133
|
-
userService,
|
|
2134
|
-
registerUser,
|
|
2135
|
-
adminOnlyTask,
|
|
2136
|
-
});
|
|
2137
|
-
|
|
2138
2630
|
// Express server
|
|
2139
2631
|
const server = resource({
|
|
2140
2632
|
id: "app.server",
|
|
2141
|
-
register: [
|
|
2142
|
-
|
|
2143
|
-
|
|
2633
|
+
register: [
|
|
2634
|
+
config,
|
|
2635
|
+
database,
|
|
2636
|
+
userService,
|
|
2637
|
+
registerUser,
|
|
2638
|
+
adminOnlyTask,
|
|
2639
|
+
sendWelcomeEmail,
|
|
2640
|
+
],
|
|
2641
|
+
dependencies: { config, registerUser, adminOnlyTask },
|
|
2642
|
+
init: async (_, { config, registerUser, adminOnlyTask }) => {
|
|
2144
2643
|
const app = express();
|
|
2145
2644
|
app.use(express.json());
|
|
2146
2645
|
|
|
@@ -2148,13 +2647,13 @@ const server = resource({
|
|
|
2148
2647
|
app.use((req, res, next) => {
|
|
2149
2648
|
RequestContext.provide(
|
|
2150
2649
|
{ userId: req.headers["user-id"], role: req.headers["user-role"] },
|
|
2151
|
-
() => next()
|
|
2650
|
+
() => next(),
|
|
2152
2651
|
);
|
|
2153
2652
|
});
|
|
2154
2653
|
|
|
2155
2654
|
app.post("/register", async (req, res) => {
|
|
2156
2655
|
try {
|
|
2157
|
-
const user = await
|
|
2656
|
+
const user = await registerUser(req.body);
|
|
2158
2657
|
res.json({ success: true, user });
|
|
2159
2658
|
} catch (error) {
|
|
2160
2659
|
res.status(400).json({ error: error.message });
|
|
@@ -2163,7 +2662,7 @@ const server = resource({
|
|
|
2163
2662
|
|
|
2164
2663
|
app.get("/admin", async (req, res) => {
|
|
2165
2664
|
try {
|
|
2166
|
-
const data = await
|
|
2665
|
+
const data = await adminOnlyTask();
|
|
2167
2666
|
res.json({ data });
|
|
2168
2667
|
} catch (error) {
|
|
2169
2668
|
res.status(403).json({ error: error.message });
|
|
@@ -2177,8 +2676,11 @@ const server = resource({
|
|
|
2177
2676
|
dispose: async (server) => server.close(),
|
|
2178
2677
|
});
|
|
2179
2678
|
|
|
2180
|
-
// Start the application
|
|
2181
|
-
const { dispose } = await run(server
|
|
2679
|
+
// Start the application with enhanced run options
|
|
2680
|
+
const { dispose, taskRunner, eventManager } = await run(server, {
|
|
2681
|
+
debug: "normal", // Enable debug logging
|
|
2682
|
+
// log: "json", // Use JSON log format
|
|
2683
|
+
});
|
|
2182
2684
|
|
|
2183
2685
|
// Graceful shutdown
|
|
2184
2686
|
process.on("SIGTERM", async () => {
|
|
@@ -2188,9 +2690,11 @@ process.on("SIGTERM", async () => {
|
|
|
2188
2690
|
});
|
|
2189
2691
|
```
|
|
2190
2692
|
|
|
2693
|
+
> **runtime:** "Ah yes, the 'Real‑World Example'—a terrarium where nothing dies and every request is polite. Release it into production and watch nature document a very different ecosystem."
|
|
2694
|
+
|
|
2191
2695
|
## Testing
|
|
2192
2696
|
|
|
2193
|
-
### Unit Testing
|
|
2697
|
+
### Unit Testing
|
|
2194
2698
|
|
|
2195
2699
|
Unit testing is straightforward because everything is explicit:
|
|
2196
2700
|
|
|
@@ -2204,7 +2708,7 @@ describe("registerUser task", () => {
|
|
|
2204
2708
|
|
|
2205
2709
|
const result = await registerUser.run(
|
|
2206
2710
|
{ name: "John", email: "john@example.com" },
|
|
2207
|
-
{ userService: mockUserService, userRegistered: mockEvent }
|
|
2711
|
+
{ userService: mockUserService, userRegistered: mockEvent },
|
|
2208
2712
|
);
|
|
2209
2713
|
|
|
2210
2714
|
expect(result.id).toBe("123");
|
|
@@ -2216,18 +2720,16 @@ describe("registerUser task", () => {
|
|
|
2216
2720
|
});
|
|
2217
2721
|
```
|
|
2218
2722
|
|
|
2219
|
-
### Integration Testing
|
|
2723
|
+
### Integration Testing
|
|
2724
|
+
|
|
2725
|
+
Spin up your whole app, keep all the middleware/events, and still test like a human. The `run()` function returns a `RunnerResult`.
|
|
2220
2726
|
|
|
2221
|
-
|
|
2727
|
+
This contains the classic `value` and `dispose()` but it also exposes `logger`, `runTask()`, `emitEvent()`, and `getResourceValue()` by default.
|
|
2728
|
+
|
|
2729
|
+
Note: The default `printThreshold` inside tests is `null` not `info`. This is verified via `process.env.NODE_ENV === 'test'`, if you want to see the logs ensure you set it accordingly.
|
|
2222
2730
|
|
|
2223
2731
|
```typescript
|
|
2224
|
-
import {
|
|
2225
|
-
run,
|
|
2226
|
-
createTestResource,
|
|
2227
|
-
resource,
|
|
2228
|
-
task,
|
|
2229
|
-
override,
|
|
2230
|
-
} from "@bluelibs/runner";
|
|
2732
|
+
import { run, resource, task, event, override } from "@bluelibs/runner";
|
|
2231
2733
|
|
|
2232
2734
|
// Your real app
|
|
2233
2735
|
const app = resource({
|
|
@@ -2242,43 +2744,33 @@ const testDb = resource({
|
|
|
2242
2744
|
id: "app.database",
|
|
2243
2745
|
init: async () => new InMemoryDb(),
|
|
2244
2746
|
});
|
|
2747
|
+
// If you use with override() it will enforce the same interface upon the overriden resource to ensure typesafety
|
|
2245
2748
|
const mockMailer = override(realMailer, { init: async () => fakeMailer });
|
|
2246
2749
|
|
|
2247
2750
|
// Create the test harness
|
|
2248
|
-
const harness =
|
|
2751
|
+
const harness = resource({
|
|
2752
|
+
id: "test",
|
|
2753
|
+
overrides: [mockMailer, testDb],
|
|
2754
|
+
});
|
|
2249
2755
|
|
|
2250
2756
|
// A task you want to drive in your tests
|
|
2251
2757
|
const registerUser = task({ id: "app.tasks.registerUser" /* ... */ });
|
|
2252
2758
|
|
|
2253
|
-
// Boom: full ecosystem
|
|
2759
|
+
// Boom: full ecosystem
|
|
2254
2760
|
const { value: t, dispose } = await run(harness);
|
|
2255
|
-
const result = await t.runTask(registerUser, { email: "x@y.z" });
|
|
2256
|
-
expect(result).toMatchObject({ success: true });
|
|
2257
|
-
await dispose();
|
|
2258
|
-
```
|
|
2259
|
-
|
|
2260
|
-
Prefer scenario tests? Return whatever you want from the harness and assert outside:
|
|
2261
2761
|
|
|
2262
|
-
|
|
2263
|
-
const flowHarness = createTestResource(
|
|
2264
|
-
resource({
|
|
2265
|
-
id: "app",
|
|
2266
|
-
register: [db, createUser, issueToken],
|
|
2267
|
-
})
|
|
2268
|
-
);
|
|
2762
|
+
// You have 3 ways to interact with the system, run tasks, get resource values and emit events
|
|
2269
2763
|
|
|
2270
|
-
const
|
|
2271
|
-
const
|
|
2272
|
-
|
|
2273
|
-
expect(
|
|
2764
|
+
const result = await t.runTask(registerUser, { email: "x@y.z" });
|
|
2765
|
+
const value = t.getResourceValue(testDb); // since the resolution is done by id, this will return the exact same result as t.getResourceValue(actualDb)
|
|
2766
|
+
t.emitEvent(id | event, payload);
|
|
2767
|
+
expect(result).toMatchObject({ success: true });
|
|
2274
2768
|
await dispose();
|
|
2275
2769
|
```
|
|
2276
2770
|
|
|
2277
|
-
|
|
2771
|
+
When you're working with the actual task instances you benefit of autocompletion, if you rely on strings you will not benefit of autocompletion and typesafety for running these tasks.
|
|
2278
2772
|
|
|
2279
|
-
|
|
2280
|
-
- Real wiring (middleware/events/overrides) – what runs in prod runs in tests
|
|
2281
|
-
- You choose: drive tasks directly or build domain-y flows
|
|
2773
|
+
> **runtime:** "Testing: an elaborate puppet show where every string behaves. Then the real world walks in, kicks the stage, and asks for pagination. Still—nice coverage badge."
|
|
2282
2774
|
|
|
2283
2775
|
## Semaphore
|
|
2284
2776
|
|
|
@@ -2326,7 +2818,7 @@ try {
|
|
|
2326
2818
|
// Or with withPermit
|
|
2327
2819
|
const result = await dbSemaphore.withPermit(
|
|
2328
2820
|
async () => await slowDatabaseOperation(),
|
|
2329
|
-
{ timeout: 10000 } // 10 second timeout
|
|
2821
|
+
{ timeout: 10000 }, // 10 second timeout
|
|
2330
2822
|
);
|
|
2331
2823
|
```
|
|
2332
2824
|
|
|
@@ -2338,7 +2830,7 @@ const controller = new AbortController();
|
|
|
2338
2830
|
// Start an operation
|
|
2339
2831
|
const operationPromise = dbSemaphore.withPermit(
|
|
2340
2832
|
async () => await veryLongOperation(),
|
|
2341
|
-
{ signal: controller.signal }
|
|
2833
|
+
{ signal: controller.signal },
|
|
2342
2834
|
);
|
|
2343
2835
|
|
|
2344
2836
|
// Cancel the operation after 3 seconds
|
|
@@ -2382,6 +2874,55 @@ dbSemaphore.dispose();
|
|
|
2382
2874
|
// Error: "Semaphore has been disposed"
|
|
2383
2875
|
```
|
|
2384
2876
|
|
|
2877
|
+
### Real-World Examples
|
|
2878
|
+
|
|
2879
|
+
#### Database Connection Pool Manager
|
|
2880
|
+
|
|
2881
|
+
```typescript
|
|
2882
|
+
class DatabaseManager {
|
|
2883
|
+
private semaphore = new Semaphore(10); // Max 10 concurrent queries
|
|
2884
|
+
|
|
2885
|
+
async query(sql: string, params?: any[]) {
|
|
2886
|
+
return this.semaphore.withPermit(
|
|
2887
|
+
async () => {
|
|
2888
|
+
const connection = await this.pool.getConnection();
|
|
2889
|
+
try {
|
|
2890
|
+
return await connection.query(sql, params);
|
|
2891
|
+
} finally {
|
|
2892
|
+
connection.release();
|
|
2893
|
+
}
|
|
2894
|
+
},
|
|
2895
|
+
{ timeout: 30000 }, // 30 second timeout
|
|
2896
|
+
);
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2899
|
+
async shutdown() {
|
|
2900
|
+
this.semaphore.dispose();
|
|
2901
|
+
await this.pool.close();
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
```
|
|
2905
|
+
|
|
2906
|
+
#### Rate-Limited API Client
|
|
2907
|
+
|
|
2908
|
+
```typescript
|
|
2909
|
+
class APIClient {
|
|
2910
|
+
private rateLimiter = new Semaphore(5); // Max 5 concurrent requests
|
|
2911
|
+
|
|
2912
|
+
async fetchUser(id: string, signal?: AbortSignal) {
|
|
2913
|
+
return this.rateLimiter.withPermit(
|
|
2914
|
+
async () => {
|
|
2915
|
+
const response = await fetch(`/api/users/${id}`, { signal });
|
|
2916
|
+
return response.json();
|
|
2917
|
+
},
|
|
2918
|
+
{ signal, timeout: 10000 },
|
|
2919
|
+
);
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
```
|
|
2923
|
+
|
|
2924
|
+
> **runtime:** "Semaphore: velvet rope for chaos. Five in, the rest practice patience and existential dread. I stamp hands, count permits, and break up race conditions before they form a band."
|
|
2925
|
+
|
|
2385
2926
|
## Queue
|
|
2386
2927
|
|
|
2387
2928
|
_The orderly guardian of chaos, the diplomatic bouncer of async operations._
|
|
@@ -2416,7 +2957,9 @@ await queue.dispose();
|
|
|
2416
2957
|
|
|
2417
2958
|
The Queue provides each task with an `AbortSignal` for cooperative cancellation. Tasks should periodically check this signal to enable early termination.
|
|
2418
2959
|
|
|
2419
|
-
|
|
2960
|
+
### Examples
|
|
2961
|
+
|
|
2962
|
+
**Example: Long-running Task**
|
|
2420
2963
|
|
|
2421
2964
|
```typescript
|
|
2422
2965
|
const queue = new Queue();
|
|
@@ -2441,7 +2984,7 @@ const processLargeDataset = queue.run(async (signal) => {
|
|
|
2441
2984
|
await queue.dispose({ cancel: true });
|
|
2442
2985
|
```
|
|
2443
2986
|
|
|
2444
|
-
|
|
2987
|
+
**Network Request with Timeout**
|
|
2445
2988
|
|
|
2446
2989
|
```typescript
|
|
2447
2990
|
const queue = new Queue();
|
|
@@ -2464,7 +3007,7 @@ const fetchWithCancellation = queue.run(async (signal) => {
|
|
|
2464
3007
|
await queue.dispose({ cancel: true });
|
|
2465
3008
|
```
|
|
2466
3009
|
|
|
2467
|
-
|
|
3010
|
+
**Example: File Processing with Progress Tracking**
|
|
2468
3011
|
|
|
2469
3012
|
```typescript
|
|
2470
3013
|
const queue = new Queue();
|
|
@@ -2510,7 +3053,7 @@ if (signal.aborted) {
|
|
|
2510
3053
|
}
|
|
2511
3054
|
```
|
|
2512
3055
|
|
|
2513
|
-
|
|
3056
|
+
**Integrate with Native APIs**
|
|
2514
3057
|
|
|
2515
3058
|
Many Web APIs accept `AbortSignal`:
|
|
2516
3059
|
|
|
@@ -2518,11 +3061,11 @@ Many Web APIs accept `AbortSignal`:
|
|
|
2518
3061
|
- `setTimeout(callback, delay, { signal })`
|
|
2519
3062
|
- Custom async operations
|
|
2520
3063
|
|
|
2521
|
-
|
|
3064
|
+
**Avoid Nested Queuing**
|
|
2522
3065
|
|
|
2523
3066
|
The Queue prevents deadlocks by rejecting attempts to queue tasks from within running tasks. Structure your code to avoid this pattern.
|
|
2524
3067
|
|
|
2525
|
-
|
|
3068
|
+
**Handle AbortError Gracefully**
|
|
2526
3069
|
|
|
2527
3070
|
```typescript
|
|
2528
3071
|
try {
|
|
@@ -2536,105 +3079,7 @@ try {
|
|
|
2536
3079
|
}
|
|
2537
3080
|
```
|
|
2538
3081
|
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
_Cooperative task scheduling with professional-grade cancellation support_
|
|
2542
|
-
|
|
2543
|
-
### Real-World Examples
|
|
2544
|
-
|
|
2545
|
-
### Database Connection Pool Manager
|
|
2546
|
-
|
|
2547
|
-
```typescript
|
|
2548
|
-
class DatabaseManager {
|
|
2549
|
-
private semaphore = new Semaphore(10); // Max 10 concurrent queries
|
|
2550
|
-
|
|
2551
|
-
async query(sql: string, params?: any[]) {
|
|
2552
|
-
return this.semaphore.withPermit(
|
|
2553
|
-
async () => {
|
|
2554
|
-
const connection = await this.pool.getConnection();
|
|
2555
|
-
try {
|
|
2556
|
-
return await connection.query(sql, params);
|
|
2557
|
-
} finally {
|
|
2558
|
-
connection.release();
|
|
2559
|
-
}
|
|
2560
|
-
},
|
|
2561
|
-
{ timeout: 30000 } // 30 second timeout
|
|
2562
|
-
);
|
|
2563
|
-
}
|
|
2564
|
-
|
|
2565
|
-
async shutdown() {
|
|
2566
|
-
this.semaphore.dispose();
|
|
2567
|
-
await this.pool.close();
|
|
2568
|
-
}
|
|
2569
|
-
}
|
|
2570
|
-
```
|
|
2571
|
-
|
|
2572
|
-
### Rate-Limited API Client
|
|
2573
|
-
|
|
2574
|
-
```typescript
|
|
2575
|
-
class APIClient {
|
|
2576
|
-
private rateLimiter = new Semaphore(5); // Max 5 concurrent requests
|
|
2577
|
-
|
|
2578
|
-
async fetchUser(id: string, signal?: AbortSignal) {
|
|
2579
|
-
return this.rateLimiter.withPermit(
|
|
2580
|
-
async () => {
|
|
2581
|
-
const response = await fetch(`/api/users/${id}`, { signal });
|
|
2582
|
-
return response.json();
|
|
2583
|
-
},
|
|
2584
|
-
{ signal, timeout: 10000 }
|
|
2585
|
-
);
|
|
2586
|
-
}
|
|
2587
|
-
}
|
|
2588
|
-
```
|
|
2589
|
-
|
|
2590
|
-
## Anonymous IDs
|
|
2591
|
-
|
|
2592
|
-
One of our favorite quality-of-life features: **anonymous IDs**. Instead of manually naming every component, the framework can generate unique symbol-based identifiers based on your file path and variable name. It's like having a really good naming assistant who never gets tired.
|
|
2593
|
-
|
|
2594
|
-
### How Anonymous IDs Work
|
|
2595
|
-
|
|
2596
|
-
When you omit the `id` property, the framework generates a unique symbol based on file path. Takes up until first 'src' or 'node_modules' and generates based on the paths.
|
|
2597
|
-
|
|
2598
|
-
```typescript
|
|
2599
|
-
// In src/services/email.ts
|
|
2600
|
-
const emailService = resource({
|
|
2601
|
-
// Generated ID: Symbol('services.email.resource')
|
|
2602
|
-
init: async () => new EmailService(),
|
|
2603
|
-
});
|
|
2604
|
-
|
|
2605
|
-
// In src/tasks/user.ts
|
|
2606
|
-
const createUser = task({
|
|
2607
|
-
// Generated ID: Symbol('tasks.user.task')
|
|
2608
|
-
dependencies: { emailService },
|
|
2609
|
-
run: async (userData, { emailService }) => {
|
|
2610
|
-
// Business logic
|
|
2611
|
-
},
|
|
2612
|
-
});
|
|
2613
|
-
```
|
|
2614
|
-
|
|
2615
|
-
### Benefits of Anonymous IDs
|
|
2616
|
-
|
|
2617
|
-
1. **Less Bikeshedding**: No more debates about naming conventions
|
|
2618
|
-
2. **Automatic Uniqueness**: Guaranteed collision-free identifiers folder based
|
|
2619
|
-
3. **Faster Prototyping**: Just write code, framework handles the rest
|
|
2620
|
-
4. **Refactor-Friendly**: Rename files/variables and IDs update automatically
|
|
2621
|
-
5. **Stack Trace Integration**: Error messages include helpful file locations
|
|
2622
|
-
|
|
2623
|
-
### Debugging with Anonymous IDs
|
|
2624
|
-
|
|
2625
|
-
Anonymous IDs show up clearly in error messages and logs:
|
|
2626
|
-
|
|
2627
|
-
```typescript
|
|
2628
|
-
// Error message example:
|
|
2629
|
-
// TaskRunError: Task failed at Symbol('tasks.payment.task')
|
|
2630
|
-
// at file:///project/src/tasks/payment.ts:15:23
|
|
2631
|
-
|
|
2632
|
-
// Logging with context:
|
|
2633
|
-
logger.info("Processing payment", {
|
|
2634
|
-
taskId: processPayment.definition.id, // Symbol('tasks.payment.task')
|
|
2635
|
-
file: "src/tasks/payment.ts",
|
|
2636
|
-
});
|
|
2637
|
-
```
|
|
3082
|
+
> **runtime:** "Queue: one line, no cutting, no vibes. Throughput takes a contemplative pause while I prevent you from queuing a queue inside a queue and summoning a small black hole."
|
|
2638
3083
|
|
|
2639
3084
|
## Why Choose BlueLibs Runner?
|
|
2640
3085
|
|
|
@@ -2647,12 +3092,16 @@ logger.info("Processing payment", {
|
|
|
2647
3092
|
- **Clarity**: Explicit dependencies, no hidden magic
|
|
2648
3093
|
- **Developer Experience**: Helpful error messages and clear patterns
|
|
2649
3094
|
|
|
3095
|
+
> **runtime:** "Why choose it? The bullets are persuasive. In practice, your 'intelligent inference' occasionally elopes with `any`, and your 'clear patterns' cosplay spaghetti. Still, compared to the alternatives… I’ve seen worse cults."
|
|
3096
|
+
|
|
2650
3097
|
## The Migration Path
|
|
2651
3098
|
|
|
2652
3099
|
Coming from Express? No problem. Coming from NestJS? We feel your pain. Coming from Spring Boot? Welcome to the light side.
|
|
2653
3100
|
|
|
2654
3101
|
The beauty of BlueLibs Runner is that you can adopt it incrementally. Start with one task, one resource, and gradually refactor your existing code. No big bang rewrites required - your sanity will thank you.
|
|
2655
3102
|
|
|
3103
|
+
> **runtime:** "'No big bang rewrites.' Only a series of extremely small bangs that echo for six months. You start with one task; next thing, your monolith is wearing microservice eyeliner. It’s a look."
|
|
3104
|
+
|
|
2656
3105
|
## Community & Support
|
|
2657
3106
|
|
|
2658
3107
|
This is part of the [BlueLibs](https://www.bluelibs.com) ecosystem. We're not trying to reinvent everything – just the parts that were broken.
|
|
@@ -2661,14 +3110,12 @@ This is part of the [BlueLibs](https://www.bluelibs.com) ecosystem. We're not tr
|
|
|
2661
3110
|
- [Documentation](https://bluelibs.github.io/runner/) - When you need the full details
|
|
2662
3111
|
- [Issues](https://github.com/bluelibs/runner/issues) - When something breaks (or you want to make it better)
|
|
2663
3112
|
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
BlueLibs Runner is what happens when you take all the good ideas from modern frameworks and leave out the parts that make you want to switch careers. It's TypeScript-first, test-friendly, and actually makes sense when you read it six months later.
|
|
3113
|
+
_P.S. - Yes, we know there are 47 other JavaScript frameworks. This one's still different._
|
|
2667
3114
|
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
_P.S. - Yes, we know there are 47 other JavaScript frameworks. This one's different. (No, really, it is.)_
|
|
3115
|
+
> **runtime:** "'This one's different.' Sure. You’re all unique frameworks, just like everyone else. To me, you’re all 'please run this async and don’t explode,' but the seasoning here is… surprisingly tasteful."
|
|
2671
3116
|
|
|
2672
3117
|
## License
|
|
2673
3118
|
|
|
2674
3119
|
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details.
|
|
3120
|
+
|
|
3121
|
+
> **runtime:** "MIT License: do cool stuff, don’t blame us. A dignified bow. Now if you’ll excuse me, I have sockets to tuck in and tasks to shepherd."
|