@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/README.md +34 -4
- package/dist/index.cjs +440 -221
- package/dist/index.d.ts +386 -107
- package/dist/index.mjs +422 -211
- package/package.json +11 -13
- package/scripts/setup-skills.js +78 -0
- package/skills/wooksjs-event-core/.gitkeep +0 -0
- package/skills/wooksjs-event-core/SKILL.md +50 -0
- package/skills/wooksjs-event-core/composables.md +200 -0
- package/skills/wooksjs-event-core/context.md +270 -0
- package/skills/wooksjs-event-core/core.md +91 -0
- package/skills/wooksjs-event-core/primitives.md +213 -0
package/package.json
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wooksjs/event-core",
|
|
3
|
-
"version": "0.
|
|
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
|