@wooksjs/event-core 0.6.5 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,17 +1,12 @@
1
1
  {
2
2
  "name": "@wooksjs/event-core",
3
- "version": "0.6.5",
3
+ "version": "0.7.0",
4
4
  "description": "@wooksjs/event-core",
5
5
  "keywords": [
6
- "api",
7
- "app",
8
6
  "composables",
7
+ "context",
8
+ "event",
9
9
  "framework",
10
- "http",
11
- "prostojs",
12
- "rest",
13
- "restful",
14
- "web",
15
10
  "wooks"
16
11
  ],
17
12
  "homepage": "https://github.com/wooksjs/wooksjs/tree/main/packages/event-core#readme",
@@ -25,8 +20,13 @@
25
20
  "url": "git+https://github.com/wooksjs/wooksjs.git",
26
21
  "directory": "packages/event-core"
27
22
  },
23
+ "bin": {
24
+ "wooksjs-event-core-skill": "./scripts/setup-skills.js"
25
+ },
28
26
  "files": [
29
- "dist"
27
+ "dist",
28
+ "skills",
29
+ "scripts/setup-skills.js"
30
30
  ],
31
31
  "main": "dist/index.cjs",
32
32
  "module": "dist/index.mjs",
@@ -39,14 +39,12 @@
39
39
  "import": "./dist/index.mjs"
40
40
  }
41
41
  },
42
- "dependencies": {
43
- "@prostojs/logger": "^0.4.3"
44
- },
45
42
  "devDependencies": {
46
43
  "typescript": "^5.9.3",
47
44
  "vitest": "^3.2.4"
48
45
  },
49
46
  "scripts": {
50
- "build": "rolldown -c ../../rolldown.config.mjs"
47
+ "build": "rolldown -c ../../rolldown.config.mjs",
48
+ "setup-skills": "node ./scripts/setup-skills.js"
51
49
  }
52
50
  }
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ /* prettier-ignore */
3
+ import fs from 'fs'
4
+ import path from 'path'
5
+ import os from 'os'
6
+ import { fileURLToPath } from 'url'
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
9
+
10
+ const SKILL_NAME = 'wooksjs-event-core'
11
+ const SKILL_SRC = path.join(__dirname, '..', 'skills', SKILL_NAME)
12
+
13
+ if (!fs.existsSync(SKILL_SRC)) {
14
+ console.error(`No skills found at ${SKILL_SRC}`)
15
+ console.error('Add your SKILL.md files to the skills/' + SKILL_NAME + '/ directory first.')
16
+ process.exit(1)
17
+ }
18
+
19
+ const AGENTS = {
20
+ 'Claude Code': { dir: '.claude/skills', global: path.join(os.homedir(), '.claude', 'skills') },
21
+ 'Cursor': { dir: '.cursor/skills', global: path.join(os.homedir(), '.cursor', 'skills') },
22
+ 'Windsurf': { dir: '.windsurf/skills', global: path.join(os.homedir(), '.windsurf', 'skills') },
23
+ 'Codex': { dir: '.codex/skills', global: path.join(os.homedir(), '.codex', 'skills') },
24
+ 'OpenCode': { dir: '.opencode/skills', global: path.join(os.homedir(), '.opencode', 'skills') },
25
+ }
26
+
27
+ const args = process.argv.slice(2)
28
+ const isGlobal = args.includes('--global') || args.includes('-g')
29
+ const isPostinstall = args.includes('--postinstall')
30
+ let installed = 0, skipped = 0
31
+ const installedDirs = []
32
+
33
+ for (const [agentName, cfg] of Object.entries(AGENTS)) {
34
+ const targetBase = isGlobal ? cfg.global : path.join(process.cwd(), cfg.dir)
35
+ const agentRootDir = path.dirname(cfg.global) // Check if the agent has ever been installed globally
36
+
37
+ // In postinstall mode: silently skip agents that aren't set up globally
38
+ if (isPostinstall || isGlobal) {
39
+ if (!fs.existsSync(agentRootDir)) { skipped++; continue }
40
+ }
41
+
42
+ const dest = path.join(targetBase, SKILL_NAME)
43
+ try {
44
+ fs.mkdirSync(dest, { recursive: true })
45
+ fs.cpSync(SKILL_SRC, dest, { recursive: true })
46
+ console.log(`✅ ${agentName}: installed to ${dest}`)
47
+ installed++
48
+ if (!isGlobal) installedDirs.push(cfg.dir + '/' + SKILL_NAME)
49
+ } catch (err) {
50
+ console.warn(`⚠️ ${agentName}: failed — ${err.message}`)
51
+ }
52
+ }
53
+
54
+ // Add locally-installed skill dirs to .gitignore
55
+ if (!isGlobal && installedDirs.length > 0) {
56
+ const gitignorePath = path.join(process.cwd(), '.gitignore')
57
+ let gitignoreContent = ''
58
+ try { gitignoreContent = fs.readFileSync(gitignorePath, 'utf8') } catch {}
59
+ const linesToAdd = installedDirs.filter(d => !gitignoreContent.includes(d))
60
+ if (linesToAdd.length > 0) {
61
+ const hasHeader = gitignoreContent.includes('# AI agent skills')
62
+ const block = (gitignoreContent && !gitignoreContent.endsWith('\n') ? '\n' : '')
63
+ + (hasHeader ? '' : '\n# AI agent skills (auto-generated by setup-skills)\n')
64
+ + linesToAdd.join('\n') + '\n'
65
+ fs.appendFileSync(gitignorePath, block)
66
+ console.log(`📝 Added ${linesToAdd.length} entries to .gitignore`)
67
+ }
68
+ }
69
+
70
+ if (installed === 0 && isPostinstall) {
71
+ // Silence is fine — no agents present, nothing to do
72
+ } else if (installed === 0 && skipped === Object.keys(AGENTS).length) {
73
+ console.log('No agent directories detected. Try --global or run without it for project-local install.')
74
+ } else if (installed === 0) {
75
+ console.log('Nothing installed. Run without --global to install project-locally.')
76
+ } else {
77
+ console.log(`\n✨ Done! Restart your AI agent to pick up the "${SKILL_NAME}" skill.`)
78
+ }
File without changes
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: wooksjs-event-core
3
+ description: Use this skill when working with @wooksjs/event-core — to create typed context slots with key(), build lazy cached computations with cached() and cachedBy(), define composables with defineWook(), define event kind schemas with defineEventKind() and slot(), manage EventContext lifecycle with run()/current()/createEventContext(), or build custom Wooks adapters. Covers useLogger(), useRouteParams(), useEventId(), and AsyncLocalStorage-based context propagation.
4
+ ---
5
+
6
+ # @wooksjs/event-core
7
+
8
+ Typed, per-event context with lazy cached computations, composable API (`defineWook`), and `AsyncLocalStorage` propagation. The foundation layer for all `@wooksjs` adapters (HTTP, CLI, workflows).
9
+
10
+ ## How to use this skill
11
+
12
+ Read the domain file that matches the task. Do not load all files — only what you need.
13
+
14
+ | Domain | File | Load when... |
15
+ | --------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
16
+ | Core concepts & setup | [core.md](core.md) | Starting a new project, understanding the mental model, seeing all exports |
17
+ | Primitives | [primitives.md](primitives.md) | Creating typed slots (`key`), lazy computations (`cached`, `cachedBy`), or event kind schemas (`slot`, `defineEventKind`) |
18
+ | Composables | [composables.md](composables.md) | Building custom composables with `defineWook`, using built-in composables (`useRouteParams`, `useEventId`, `useLogger`) |
19
+ | Context & runtime | [context.md](context.md) | Managing `EventContext` lifecycle, `run`/`current`/`createEventContext`, async propagation, performance optimization |
20
+
21
+ ## Quick reference
22
+
23
+ ```ts
24
+ import {
25
+ // primitives
26
+ key,
27
+ cached,
28
+ cachedBy,
29
+ slot,
30
+ defineEventKind,
31
+ defineWook,
32
+ // context
33
+ EventContext,
34
+ run,
35
+ current,
36
+ tryGetCurrent,
37
+ createEventContext,
38
+ // composables
39
+ useRouteParams,
40
+ useEventId,
41
+ useLogger,
42
+ // standard keys
43
+ routeParamsKey,
44
+ eventTypeKey,
45
+ // observability
46
+ ContextInjector,
47
+ getContextInjector,
48
+ replaceContextInjector,
49
+ } from '@wooksjs/event-core'
50
+ ```
@@ -0,0 +1,200 @@
1
+ # Composables — @wooksjs/event-core
2
+
3
+ > `defineWook` and the built-in composables: `useRouteParams`, `useEventId`, `useLogger`.
4
+
5
+ ## Concepts
6
+
7
+ A **composable** is a function that accesses per-event contextual data. Call it — get typed data back. No arguments needed (context resolved via `AsyncLocalStorage`).
8
+
9
+ `defineWook(factory)` is the primitive that creates composables. The factory runs once per `EventContext`, and the result is cached. Every subsequent call within the same event returns the same object.
10
+
11
+ All built-in composables across the Wooks ecosystem (`useRequest`, `useResponse`, `useBody`, `useCookies`, etc.) are built with `defineWook`.
12
+
13
+ ## API Reference
14
+
15
+ ### `defineWook<T>(factory: (ctx: EventContext) => T): (ctx?: EventContext) => T`
16
+
17
+ Creates a composable with per-event caching. Internally wraps the factory in a `cached()` slot.
18
+
19
+ ```ts
20
+ import { defineWook, defineEventKind, slot } from '@wooksjs/event-core'
21
+
22
+ const http = defineEventKind('http', {
23
+ method: slot<string>(),
24
+ path: slot<string>(),
25
+ })
26
+
27
+ const useRequest = defineWook((ctx) => ({
28
+ method: ctx.get(http.keys.method),
29
+ path: ctx.get(http.keys.path),
30
+ }))
31
+
32
+ // In a handler:
33
+ const { method, path } = useRequest()
34
+ ```
35
+
36
+ **Caching guarantee:** The factory runs exactly once per event context. Subsequent calls return the same object:
37
+
38
+ ```ts
39
+ const a = useRequest()
40
+ const b = useRequest()
41
+ a === b // true — same object reference
42
+ ```
43
+
44
+ **Optional explicit context:** Pass `ctx` to skip the `AsyncLocalStorage` lookup:
45
+
46
+ ```ts
47
+ const ctx = current()
48
+ const { method } = useRequest(ctx)
49
+ const { parseBody } = useBody(ctx)
50
+ ```
51
+
52
+ ### `useRouteParams<T>(ctx?): { params: T; get: (name) => T[K] }`
53
+
54
+ Returns route parameters set by the router. Generic `T` defaults to `Record<string, string | string[]>`.
55
+
56
+ ```ts
57
+ import { useRouteParams } from '@wooksjs/event-core'
58
+
59
+ // Given route /users/:id
60
+ const { params, get } = useRouteParams<{ id: string }>()
61
+ params.id // 'abc'
62
+ get('id') // 'abc'
63
+ ```
64
+
65
+ ### `useEventId(ctx?): { getId: () => string }`
66
+
67
+ Returns a lazy UUID for the current event. The UUID is generated on first call to `getId()` and cached.
68
+
69
+ ```ts
70
+ import { useEventId } from '@wooksjs/event-core'
71
+
72
+ const { getId } = useEventId()
73
+ getId() // 'a1b2c3d4-...' (stable for this event)
74
+ ```
75
+
76
+ ### `useLogger(ctx?): Logger`
77
+
78
+ Returns the logger from the current event context. Shorthand for `(ctx ?? current()).logger`.
79
+
80
+ ```ts
81
+ import { useLogger } from '@wooksjs/event-core'
82
+
83
+ const log = useLogger()
84
+ log.info('handling request')
85
+ ```
86
+
87
+ ## Common Patterns
88
+
89
+ ### Pattern: Composable with lazy properties
90
+
91
+ Wrap non-trivial computations in thunks so they only run when accessed:
92
+
93
+ ```ts
94
+ const useRequest = defineWook((ctx) => ({
95
+ method: ctx.get(http.keys.method), // cheap — direct key lookup
96
+ query: () => ctx.get(parsedQuery), // deferred until called
97
+ cookies: () => ctx.get(parsedCookies), // deferred until called
98
+ }))
99
+
100
+ // Only method is computed eagerly:
101
+ const { method } = useRequest()
102
+ // query is only computed if accessed:
103
+ const q = useRequest().query()
104
+ ```
105
+
106
+ ### Pattern: Composable with async cached values
107
+
108
+ Combine `defineWook` with `cached` for async computations:
109
+
110
+ ```ts
111
+ const parsedBody = cached(async (ctx) => {
112
+ const buf = await ctx.get(rawBody)
113
+ return JSON.parse(buf.toString())
114
+ })
115
+
116
+ const useBody = defineWook((ctx) => ({
117
+ parseBody: () => ctx.get(parsedBody),
118
+ }))
119
+
120
+ // In handler:
121
+ const { parseBody } = useBody()
122
+ const body = await parseBody() // parses once
123
+ const again = await parseBody() // cache hit, same object
124
+ ```
125
+
126
+ ### Pattern: Class-based composable for method-heavy APIs
127
+
128
+ When a composable exposes many methods, use a class to avoid per-event closure allocation:
129
+
130
+ ```ts
131
+ class ResponseState {
132
+ status = 200
133
+ body: unknown = undefined
134
+ readonly headers = new Map<string, string>()
135
+
136
+ setHeader(name: string, value: string) {
137
+ this.headers.set(name, value)
138
+ return this
139
+ }
140
+ setStatus(code: number) {
141
+ this.status = code
142
+ return this
143
+ }
144
+ json(data: unknown) {
145
+ this.body = data
146
+ return this
147
+ }
148
+ }
149
+
150
+ const useResponse = defineWook(() => new ResponseState())
151
+ ```
152
+
153
+ Methods live on the prototype — zero closures per event.
154
+
155
+ ### Pattern: Composable depending on another composable
156
+
157
+ Composables can call other composables inside their factory:
158
+
159
+ ```ts
160
+ const useCurrentUser = defineWook((ctx) => {
161
+ const { basicCredentials } = useAuthorization(ctx)
162
+ const username = basicCredentials()?.username
163
+ return {
164
+ username,
165
+ profile: async () => (username ? await db.findUser(username) : null),
166
+ }
167
+ })
168
+ ```
169
+
170
+ Pass `ctx` to avoid redundant `AsyncLocalStorage` lookups.
171
+
172
+ ### Pattern: Plain function for single-value access
173
+
174
+ `defineWook` adds caching overhead. For trivial single-value access, use a plain function:
175
+
176
+ ```ts
177
+ // Overkill — caching overhead > value cost
178
+ const useMethod = defineWook((ctx) => ctx.get(http.keys.method))
179
+
180
+ // Better — one Map.get, no caching layer
181
+ function useMethod(ctx?: EventContext): string {
182
+ return (ctx ?? current()).get(http.keys.method) ?? 'GET'
183
+ }
184
+ ```
185
+
186
+ ## Best Practices
187
+
188
+ - Use `defineWook` when the factory returns an object with multiple properties or methods
189
+ - Use plain functions for trivial single-value access
190
+ - Always accept optional `ctx` parameter in composables for performance
191
+ - Pass `ctx` explicitly when calling multiple composables in sequence
192
+ - Use thunks for non-trivial properties in the returned object
193
+ - Use classes for composables with 4+ methods
194
+
195
+ ## Gotchas
196
+
197
+ - `defineWook` factory receives `ctx` as parameter — use it instead of calling `current()` inside
198
+ - The factory runs once per context — don't put per-call logic in it (use thunks for that)
199
+ - Calling a composable outside an event context throws `No active event context`
200
+ - Different event contexts get different composable instances — the cache is per-context
@@ -0,0 +1,270 @@
1
+ # Context & runtime — @wooksjs/event-core
2
+
3
+ > `EventContext` class, `run`/`current`/`createEventContext`, async propagation, and performance.
4
+
5
+ ## Concepts
6
+
7
+ Every event in Wooks gets its own `EventContext` — a lightweight container backed by `Map<number, unknown>`. The context is propagated through the async call stack via Node.js `AsyncLocalStorage`.
8
+
9
+ The lifecycle:
10
+
11
+ 1. Create an `EventContext` (or use `createEventContext`), optionally linking a `parent` context
12
+ 2. Seed it with event-specific data via `ctx.seed(kind, seeds)`
13
+ 3. Enter the `AsyncLocalStorage` scope with `run(ctx, fn)`
14
+ 4. Inside `fn`, composables resolve the context via `current()`
15
+
16
+ Adapters (HTTP, CLI, etc.) handle steps 1-3. Your handler code only interacts with composables.
17
+
18
+ ## API Reference
19
+
20
+ ### `EventContext`
21
+
22
+ ```ts
23
+ import { EventContext } from '@wooksjs/event-core'
24
+
25
+ const ctx = new EventContext({ logger })
26
+ ```
27
+
28
+ **Constructor options:**
29
+
30
+ ```ts
31
+ interface EventContextOptions {
32
+ logger: Logger
33
+ parent?: EventContext
34
+ }
35
+ ```
36
+
37
+ Every context requires a `Logger`. Access it via `ctx.logger`.
38
+
39
+ When `parent` is provided, the child context forms a **parent chain**. Lookups via `get()`, `set()`, and `has()` traverse the chain (local first, then parent, then grandparent, etc.). This replaces the old pattern of attaching multiple kinds to a single context — instead, create child contexts with parent links:
40
+
41
+ ```ts
42
+ const parentCtx = new EventContext({ logger })
43
+ parentCtx.seed(httpKind, { method: 'GET', path: '/api' })
44
+
45
+ const childCtx = new EventContext({ logger, parent: parentCtx })
46
+ childCtx.seed(workflowKind, { triggerId: 'deploy-42' })
47
+
48
+ // child can see its own data AND parent data
49
+ childCtx.get(httpKind.keys.method) // 'GET' (found in parent)
50
+ childCtx.get(workflowKind.keys.triggerId) // 'deploy-42' (found locally)
51
+ ```
52
+
53
+ **Methods:**
54
+
55
+ #### `ctx.get<T>(accessor: Key<T> | Cached<T>): T`
56
+
57
+ Get a value by key or cached accessor. **Traverses the parent chain:** checks local context first, then parent, grandparent, and so on.
58
+
59
+ - For `Key<T>`: returns the first value found in the chain. Throws `Key "name" is not set` if not found in any context.
60
+ - For `Cached<T>`: if a computed value is found anywhere in the parent chain, returns it (no re-computation). If not found in any context, computes and caches the result **locally**.
61
+
62
+ #### `ctx.set<T>(key: Key<T> | Cached<T>, value: T): void`
63
+
64
+ Set a value for a key. **Traverses the parent chain:** if the key already exists somewhere in the chain, writes to that context. If the key is new (not found in any ancestor), writes locally. Works with `undefined`, `null`, and falsy values.
65
+
66
+ #### `ctx.has(accessor: Accessor<any>): boolean`
67
+
68
+ Check if a slot has been set or computed. **Traverses the parent chain.** Returns `true` if the slot is found in any context in the chain. Returns `false` for unset keys and uncomputed cached values across the entire chain.
69
+
70
+ #### `ctx.getOwn<T>(accessor: Key<T> | Cached<T>): T`
71
+
72
+ Like `get()`, but **local only** — does not traverse the parent chain. Returns the value from this context only. Throws if not set locally.
73
+
74
+ #### `ctx.setOwn<T>(key: Key<T> | Cached<T>, value: T): void`
75
+
76
+ Like `set()`, but **local only** — always writes to this context, even if the key exists in a parent.
77
+
78
+ #### `ctx.hasOwn(accessor: Accessor<any>): boolean`
79
+
80
+ Like `has()`, but **local only** — returns `true` only if the slot is set in this context, ignoring parents.
81
+
82
+ #### `ctx.seed(kind, seeds[, fn])`
83
+
84
+ Populate context from an `EventKind` schema (previously called `attach`):
85
+
86
+ ```ts
87
+ const http = defineEventKind('http', {
88
+ method: slot<string>(),
89
+ path: slot<string>(),
90
+ })
91
+
92
+ ctx.seed(http, { method: 'GET', path: '/api' })
93
+ ctx.get(http.keys.method) // 'GET'
94
+ ```
95
+
96
+ Also sets `eventTypeKey` to the kind's name.
97
+
98
+ Overloaded: can accept an optional callback that runs immediately after seeding:
99
+
100
+ ```ts
101
+ ctx.seed(http, seeds, () => {
102
+ // seeds are available here
103
+ })
104
+ ```
105
+
106
+ ### `run<R>(ctx: EventContext, fn: () => R): R`
107
+
108
+ Execute `fn` within the `AsyncLocalStorage` scope of `ctx`. Returns the fn's return value. Works with async functions.
109
+
110
+ ```ts
111
+ const ctx = new EventContext({ logger })
112
+ const result = run(ctx, () => {
113
+ // current() returns ctx here
114
+ return 'hello'
115
+ })
116
+ // result === 'hello'
117
+ ```
118
+
119
+ Supports nesting — inner `run` creates a child scope:
120
+
121
+ ```ts
122
+ run(outer, () => {
123
+ current() === outer // true
124
+ run(inner, () => {
125
+ current() === inner // true
126
+ })
127
+ current() === outer // true (restored)
128
+ })
129
+ ```
130
+
131
+ ### `current(): EventContext`
132
+
133
+ Get the active `EventContext` from the `AsyncLocalStorage` scope. Throws `[Wooks] No active event context` if called outside a `run` scope.
134
+
135
+ ### `tryGetCurrent(): EventContext | undefined`
136
+
137
+ Like `current()` but returns `undefined` instead of throwing.
138
+
139
+ ### `createEventContext(options, fn): R`
140
+
141
+ ### `createEventContext(options, kind, seeds, fn): R`
142
+
143
+ Convenience: creates a context, optionally seeds a kind, and runs `fn` inside `AsyncLocalStorage`:
144
+
145
+ ```ts
146
+ // Bare context
147
+ createEventContext({ logger }, () => {
148
+ const ctx = current()
149
+ // ...
150
+ })
151
+
152
+ // With kind + seeds
153
+ createEventContext({ logger }, httpKind, { method: 'GET', path: '/' }, () => {
154
+ current().get(httpKind.keys.method) // 'GET'
155
+ })
156
+ ```
157
+
158
+ Returns the fn's return value. Works with async callbacks.
159
+
160
+ ### `useLogger(ctx?: EventContext): Logger`
161
+
162
+ Shorthand for `(ctx ?? current()).logger`.
163
+
164
+ ## Version safety
165
+
166
+ `@wooksjs/event-core` uses a global `Symbol.for('wooks.core.asyncStorage')` to ensure a single `AsyncLocalStorage` instance across the process. If two incompatible versions are loaded, it throws at import time:
167
+
168
+ ```
169
+ [wooks] Incompatible versions of @wooksjs/event-core detected: existing v0.6.6, loading v0.7.0
170
+ ```
171
+
172
+ This prevents subtle bugs from multiple context stores.
173
+
174
+ ## Common Patterns
175
+
176
+ ### Pattern: Custom adapter
177
+
178
+ Build an adapter that creates contexts and runs handlers:
179
+
180
+ ```ts
181
+ import { EventContext, defineEventKind, slot, run } from '@wooksjs/event-core'
182
+
183
+ const myKind = defineEventKind('my-event', {
184
+ data: slot<unknown>(),
185
+ })
186
+
187
+ function handleEvent(data: unknown, handler: () => unknown, parent?: EventContext) {
188
+ const ctx = new EventContext({ logger: console, parent })
189
+ ctx.seed(myKind, { data })
190
+ return run(ctx, handler)
191
+ }
192
+ ```
193
+
194
+ ### Pattern: Child contexts with parent links
195
+
196
+ Instead of attaching multiple kinds to a single context, create child contexts with parent links. Each child sees its own data plus everything in the parent chain:
197
+
198
+ ```ts
199
+ createEventContext({ logger }, httpKind, httpSeeds, () => {
200
+ const parentCtx = current()
201
+
202
+ // Create a child context for workflow-specific data
203
+ const childCtx = new EventContext({ logger, parent: parentCtx })
204
+ childCtx.seed(workflowKind, { triggerId: 'deploy-42', payload })
205
+
206
+ run(childCtx, () => {
207
+ // Both HTTP and workflow composables work
208
+ const { method } = useRequest() // found via parent chain
209
+ const { triggerId } = useWorkflow() // found locally
210
+ })
211
+ })
212
+ ```
213
+
214
+ The parent chain allows layered contexts — for example, a base HTTP context can be shared across sub-handlers, each with their own child context carrying handler-specific state.
215
+
216
+ ### Pattern: Resolve context once for hot paths
217
+
218
+ Each composable call without `ctx` hits `AsyncLocalStorage.getStore()`. For hot paths:
219
+
220
+ ```ts
221
+ // 4 ALS lookups
222
+ const { method } = useRequest()
223
+ const { parseBody } = useBody()
224
+ const session = getCookie('session')
225
+ const log = useLogger()
226
+
227
+ // 1 ALS lookup
228
+ const ctx = current()
229
+ const { method } = useRequest(ctx)
230
+ const { parseBody } = useBody(ctx)
231
+ const session = getCookie('session', ctx)
232
+ const log = ctx.logger
233
+ ```
234
+
235
+ ## ContextInjector (observability)
236
+
237
+ `ContextInjector` is a hook point for observability tools (OpenTelemetry, etc.). The default implementation is a no-op pass-through.
238
+
239
+ ```ts
240
+ import { ContextInjector, replaceContextInjector } from '@wooksjs/event-core'
241
+
242
+ class OtelInjector extends ContextInjector<string> {
243
+ with<T>(name: string, attributes: Record<string, any>, cb: () => T): T {
244
+ return tracer.startActiveSpan(name, { attributes }, () => cb())
245
+ }
246
+ }
247
+
248
+ replaceContextInjector(new OtelInjector())
249
+ ```
250
+
251
+ The injector's `with()` method wraps `createEventContext` callbacks and handler invocations.
252
+
253
+ ## Best Practices
254
+
255
+ - Let adapters handle context creation — in handler code, just use composables
256
+ - Pass `ctx` explicitly in hot paths with multiple composable calls
257
+ - Use `tryGetCurrent()` in library code that may run outside event scope
258
+ - Keep `Logger` simple — the interface is `info/warn/error/debug` with optional `topic()`
259
+ - Prefer parent-linked child contexts over seeding multiple kinds into one context — the parent chain keeps each layer's data isolated while still accessible
260
+ - Use `getOwn()`/`setOwn()`/`hasOwn()` when you need to guarantee local-only access and avoid unintended reads from or writes to parent contexts
261
+
262
+ ## Gotchas
263
+
264
+ - `current()` throws outside `run()` scope — use `tryGetCurrent()` if that's expected
265
+ - `AsyncLocalStorage` propagates through `await`, `setTimeout`, `setImmediate`, and Promise chains
266
+ - `EventContext` is not thread-safe — it's designed for single-event, single-async-chain use
267
+ - The global singleton `AsyncLocalStorage` means all `@wooksjs/event-core` consumers in a process share context — version mismatches are caught at import time
268
+ - `set()` writes to the first context in the parent chain where the key exists — if you need to shadow a parent value locally, use `setOwn()`
269
+ - `get()` on a `Cached<T>` found in a parent returns the parent's cached value without re-computing — if you need a fresh local computation, use `getOwn()`
270
+ - Parent chain traversal has O(depth) cost per lookup — keep chains shallow for performance