@wooksjs/event-core 0.7.8 → 0.7.10

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/dist/index.cjs CHANGED
@@ -305,7 +305,7 @@ function resetContextInjector() {
305
305
  //#region packages/event-core/src/storage.ts
306
306
  const STORAGE_KEY = Symbol.for("wooks.core.asyncStorage");
307
307
  const VERSION_KEY = Symbol.for("wooks.core.asyncStorage.version");
308
- const CURRENT_VERSION = "0.7.7";
308
+ const CURRENT_VERSION = "0.7.9";
309
309
  const _g = globalThis;
310
310
  if (_g[STORAGE_KEY]) {
311
311
  if (_g[VERSION_KEY] !== CURRENT_VERSION) throw new Error(`[wooks] Incompatible versions of @wooksjs/event-core detected: existing v${_g[VERSION_KEY]}, loading v${CURRENT_VERSION}. All packages must use the same @wooksjs/event-core version.`);
@@ -355,19 +355,11 @@ function current() {
355
355
  function tryGetCurrent() {
356
356
  return storage.getStore();
357
357
  }
358
- /**
359
- * Returns the logger for the current event context.
360
- *
361
- * @param ctx - Optional explicit context (defaults to `current()`)
362
- *
363
- * @example
364
- * ```ts
365
- * const logger = useLogger()
366
- * logger.info('Processing request')
367
- * ```
368
- */
369
- function useLogger(ctx) {
370
- return (ctx ?? current()).logger;
358
+ function useLogger(topicOrCtx, maybeCtx) {
359
+ const ctx = (typeof topicOrCtx === "string" ? maybeCtx : topicOrCtx) ?? current();
360
+ const logger = ctx.logger;
361
+ if (typeof topicOrCtx === "string" && logger.createTopic) return logger.createTopic(topicOrCtx);
362
+ return logger;
371
363
  }
372
364
  function createEventContext(options, kindOrFn, seedsOrUndefined, maybeFn) {
373
365
  const ctx = new EventContext(options);
package/dist/index.d.ts CHANGED
@@ -8,8 +8,8 @@ interface Logger {
8
8
  warn(msg: string, ...args: unknown[]): void;
9
9
  error(msg: string, ...args: unknown[]): void;
10
10
  debug(msg: string, ...args: unknown[]): void;
11
- /** Creates a child logger with a topic prefix (optional). */
12
- topic?: (name?: string) => Logger;
11
+ /** Creates a child logger scoped to a topic. Matches `ProstoLogger.createTopic()`. */
12
+ createTopic?: (name: string) => Logger;
13
13
  }
14
14
  /**
15
15
  * A typed, writable context slot. Created with `key<T>(name)`.
@@ -405,15 +405,23 @@ declare function tryGetCurrent(): EventContext | undefined;
405
405
  /**
406
406
  * Returns the logger for the current event context.
407
407
  *
408
- * @param ctx - Optional explicit context (defaults to `current()`)
408
+ * When called with a `topic` string, creates a child logger via
409
+ * `logger.createTopic()` (if supported). Falls back to the base
410
+ * logger when `createTopic` is not available.
409
411
  *
410
412
  * @example
411
413
  * ```ts
412
414
  * const logger = useLogger()
413
415
  * logger.info('Processing request')
416
+ *
417
+ * const scoped = useLogger('auth')
418
+ * scoped.warn('Token expired')
414
419
  * ```
415
420
  */
416
- declare function useLogger(ctx?: EventContext): Logger;
421
+ declare function useLogger(): Logger;
422
+ declare function useLogger(topic: string): Logger;
423
+ declare function useLogger(ctx: EventContext): Logger;
424
+ declare function useLogger(topic: string, ctx: EventContext): Logger;
417
425
  /**
418
426
  * Creates a new `EventContext`, makes it the active context via
419
427
  * `AsyncLocalStorage`, and runs `fn` inside it.
package/dist/index.mjs CHANGED
@@ -282,7 +282,7 @@ function resetContextInjector() {
282
282
  //#region packages/event-core/src/storage.ts
283
283
  const STORAGE_KEY = Symbol.for("wooks.core.asyncStorage");
284
284
  const VERSION_KEY = Symbol.for("wooks.core.asyncStorage.version");
285
- const CURRENT_VERSION = "0.7.7";
285
+ const CURRENT_VERSION = "0.7.9";
286
286
  const _g = globalThis;
287
287
  if (_g[STORAGE_KEY]) {
288
288
  if (_g[VERSION_KEY] !== CURRENT_VERSION) throw new Error(`[wooks] Incompatible versions of @wooksjs/event-core detected: existing v${_g[VERSION_KEY]}, loading v${CURRENT_VERSION}. All packages must use the same @wooksjs/event-core version.`);
@@ -332,19 +332,11 @@ function current() {
332
332
  function tryGetCurrent() {
333
333
  return storage.getStore();
334
334
  }
335
- /**
336
- * Returns the logger for the current event context.
337
- *
338
- * @param ctx - Optional explicit context (defaults to `current()`)
339
- *
340
- * @example
341
- * ```ts
342
- * const logger = useLogger()
343
- * logger.info('Processing request')
344
- * ```
345
- */
346
- function useLogger(ctx) {
347
- return (ctx ?? current()).logger;
335
+ function useLogger(topicOrCtx, maybeCtx) {
336
+ const ctx = (typeof topicOrCtx === "string" ? maybeCtx : topicOrCtx) ?? current();
337
+ const logger = ctx.logger;
338
+ if (typeof topicOrCtx === "string" && logger.createTopic) return logger.createTopic(topicOrCtx);
339
+ return logger;
348
340
  }
349
341
  function createEventContext(options, kindOrFn, seedsOrUndefined, maybeFn) {
350
342
  const ctx = new EventContext(options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wooksjs/event-core",
3
- "version": "0.7.8",
3
+ "version": "0.7.10",
4
4
  "description": "@wooksjs/event-core",
5
5
  "keywords": [
6
6
  "composables",
@@ -20,13 +20,8 @@
20
20
  "url": "git+https://github.com/wooksjs/wooksjs.git",
21
21
  "directory": "packages/event-core"
22
22
  },
23
- "bin": {
24
- "wooksjs-event-core-skill": "./scripts/setup-skills.js"
25
- },
26
23
  "files": [
27
- "dist",
28
- "skills",
29
- "scripts/setup-skills.js"
24
+ "dist"
30
25
  ],
31
26
  "main": "dist/index.cjs",
32
27
  "module": "dist/index.mjs",
@@ -44,7 +39,6 @@
44
39
  "vitest": "^3.2.4"
45
40
  },
46
41
  "scripts": {
47
- "build": "rolldown -c ../../rolldown.config.mjs",
48
- "setup-skills": "node ./scripts/setup-skills.js"
42
+ "build": "rolldown -c ../../rolldown.config.mjs"
49
43
  }
50
44
  }
@@ -1,78 +0,0 @@
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
@@ -1,51 +0,0 @@
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
- resetContextInjector,
50
- } from '@wooksjs/event-core'
51
- ```
@@ -1,200 +0,0 @@
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
@@ -1,288 +0,0 @@
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. Each adapter exports a **context factory** that hardcodes the kind and delegates to `createEventContext`:
179
-
180
- ```ts
181
- import { createEventContext, defineEventKind, slot } from '@wooksjs/event-core'
182
- import type { EventContextOptions, EventKindSeeds } from '@wooksjs/event-core'
183
-
184
- const myKind = defineEventKind('my-event', {
185
- data: slot<unknown>(),
186
- })
187
-
188
- // Context factory — same signature as createEventContext, minus the kind
189
- export function createMyEventContext<R>(
190
- options: EventContextOptions,
191
- seeds: EventKindSeeds<typeof myKind>,
192
- fn: () => R,
193
- ): R {
194
- return createEventContext(options, myKind, seeds, fn)
195
- }
196
-
197
- // Usage in the adapter
198
- function handleEvent(data: unknown, handler: () => unknown) {
199
- return createMyEventContext({ logger: console }, { data }, handler)
200
- }
201
- ```
202
-
203
- All built-in adapters follow this pattern: `createHttpContext`, `createCliContext`, `createWsConnectionContext`, `createWsMessageContext`, `createWfContext`, `resumeWfContext`.
204
-
205
- ### Pattern: Child contexts with parent links
206
-
207
- Instead of attaching multiple kinds to a single context, create child contexts by passing `parent` in the options. Each child sees its own data plus everything in the parent chain:
208
-
209
- ```ts
210
- createEventContext({ logger }, httpKind, httpSeeds, () => {
211
- const parentCtx = current()
212
-
213
- // Create a child context linked to the HTTP parent
214
- createEventContext(
215
- { logger, parent: parentCtx },
216
- workflowKind,
217
- { triggerId: 'deploy-42', payload },
218
- () => {
219
- // Both HTTP and workflow composables work
220
- const { method } = useRequest() // found via parent chain
221
- const { triggerId } = useWorkflow() // found locally
222
- },
223
- )
224
- })
225
- ```
226
-
227
- 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.
228
-
229
- ### Pattern: Resolve context once for hot paths
230
-
231
- Each composable call without `ctx` hits `AsyncLocalStorage.getStore()`. For hot paths:
232
-
233
- ```ts
234
- // 4 ALS lookups
235
- const { method } = useRequest()
236
- const { parseBody } = useBody()
237
- const session = getCookie('session')
238
- const log = useLogger()
239
-
240
- // 1 ALS lookup
241
- const ctx = current()
242
- const { method } = useRequest(ctx)
243
- const { parseBody } = useBody(ctx)
244
- const session = getCookie('session', ctx)
245
- const log = ctx.logger
246
- ```
247
-
248
- ## ContextInjector (observability)
249
-
250
- `ContextInjector` is a hook point for observability tools (OpenTelemetry, etc.). No injector is installed by default — `getContextInjector()` returns `null`, so all adapters run with zero overhead until one is installed.
251
-
252
- ```ts
253
- import { ContextInjector, replaceContextInjector, resetContextInjector } from '@wooksjs/event-core'
254
-
255
- class OtelInjector extends ContextInjector<string> {
256
- with<T>(name: string, attributes: Record<string, any>, cb: () => T): T {
257
- return tracer.startActiveSpan(name, { attributes }, () => cb())
258
- }
259
- }
260
-
261
- replaceContextInjector(new OtelInjector())
262
-
263
- // To disable instrumentation later:
264
- resetContextInjector()
265
- ```
266
-
267
- The injector's `with()` method wraps `createEventContext` callbacks. All adapters (HTTP, CLI, WS, WF) route through `createEventContext`, so installing an injector automatically instruments every event type.
268
-
269
- Adapter callbacks **return their result** (sync value or Promise), so `with()` can track async handler completion for span lifecycle. A `TracingInjector` can detect thenable returns and attach `.then()/.catch()` to end spans when the handler settles — no separate `onEnd()` hook needed.
270
-
271
- ## Best Practices
272
-
273
- - Let adapters handle context creation — in handler code, just use composables
274
- - Pass `ctx` explicitly in hot paths with multiple composable calls
275
- - Use `tryGetCurrent()` in library code that may run outside event scope
276
- - Keep `Logger` simple — the interface is `info/warn/error/debug` with optional `topic()`
277
- - Prefer parent-linked child contexts over seeding multiple kinds into one context — the parent chain keeps each layer's data isolated while still accessible
278
- - Use `getOwn()`/`setOwn()`/`hasOwn()` when you need to guarantee local-only access and avoid unintended reads from or writes to parent contexts
279
-
280
- ## Gotchas
281
-
282
- - `current()` throws outside `run()` scope — use `tryGetCurrent()` if that's expected
283
- - `AsyncLocalStorage` propagates through `await`, `setTimeout`, `setImmediate`, and Promise chains
284
- - `EventContext` is not thread-safe — it's designed for single-event, single-async-chain use
285
- - The global singleton `AsyncLocalStorage` means all `@wooksjs/event-core` consumers in a process share context — version mismatches are caught at import time
286
- - `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()`
287
- - `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()`
288
- - Parent chain traversal has O(depth) cost per lookup — keep chains shallow for performance
@@ -1,92 +0,0 @@
1
- # Core concepts & setup — @wooksjs/event-core
2
-
3
- > Package overview, mental model, installation, and exports map.
4
-
5
- ## What it is
6
-
7
- `@wooksjs/event-core` is the context engine underneath all Wooks adapters. It provides:
8
-
9
- - **Typed slots** (`key<T>`) — explicit get/set per event
10
- - **Lazy cached computations** (`cached`, `cachedBy`) — compute once, cache for event lifetime
11
- - **Event kind schemas** (`defineEventKind`) — typed seed bundles for domain-specific data
12
- - **Composable factories** (`defineWook`) — per-event cached functions, resolved via `AsyncLocalStorage`
13
- - **Async propagation** (`run`, `current`) — context available anywhere in the call stack
14
-
15
- ## Installation
16
-
17
- ```bash
18
- pnpm add @wooksjs/event-core
19
- ```
20
-
21
- Typically you don't install this directly — it's a dependency of `@wooksjs/event-http`, `@wooksjs/event-cli`, etc. Install it when building custom adapters or composables that don't depend on a specific event type.
22
-
23
- ## Mental model
24
-
25
- ```
26
- EventContext = Map<number, unknown> (flat, typed via key/cached accessors)
27
- │
28
- ├── key<T>(name) → writable slot, explicit set/get
29
- ├── cached<T>(fn) → read-only slot, lazy, computed on first access
30
- ├── cachedBy<K,V>(fn) → like cached but keyed — one result per unique argument
31
- │
32
- ├── defineEventKind(name, schema) → groups slots into a named seed bundle
33
- │ └── ctx.seed(kind, seeds) → populates slots from seeds
34
- │
35
- ├── defineWook(factory) → creates a composable: factory runs once per context, cached
36
- │ └── useX() → resolves from AsyncLocalStorage, returns cached result
37
- │
38
- └── parent chain → optional parent: EventContext link
39
- ├── get/set/has → traverse local → parent → grandparent → ...
40
- └── getOwn/setOwn/hasOwn → local-only (no traversal)
41
- ```
42
-
43
- Every event (HTTP request, CLI invocation, workflow step) gets its own `EventContext`. The context is propagated via `AsyncLocalStorage` — composables resolve it implicitly.
44
-
45
- ## Exports
46
-
47
- | Export | Kind | Purpose |
48
- | --------------------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------- |
49
- | `key<T>(name)` | primitive | Writable typed slot |
50
- | `cached<T>(fn)` | primitive | Lazy computed slot, cached per event |
51
- | `cachedBy<K,V>(fn)` | primitive | Lazy computed slot, cached per key per event |
52
- | `slot<T>()` | primitive | Type marker for `defineEventKind` schemas |
53
- | `defineEventKind(name, schema)` | primitive | Typed seed bundle definition |
54
- | `defineWook<T>(factory)` | primitive | Composable factory with per-event caching |
55
- | `EventContext` | class | Per-event context container (`get`, `set`, `has`, `seed`, `getOwn`, `setOwn`, `hasOwn`; optional `parent` chain) |
56
- | `run(ctx, fn)` | function | Execute `fn` within `ctx` via `AsyncLocalStorage` |
57
- | `current()` | function | Get active `EventContext` (throws if none) |
58
- | `tryGetCurrent()` | function | Get active `EventContext` or `undefined` |
59
- | `createEventContext(opts, [kind, seeds,] fn)` | function | Create context (with optional parent) + optional seed + run |
60
- | `useLogger(ctx?)` | composable | Shorthand for `(ctx ?? current()).logger` |
61
- | `useRouteParams(ctx?)` | composable | Route parameters from router |
62
- | `useEventId(ctx?)` | composable | Lazy UUID per event |
63
- | `routeParamsKey` | key | Standard key for route params |
64
- | `eventTypeKey` | key | Standard key for event type name |
65
- | `ContextInjector` | class | Observability hook point (OpenTelemetry etc.). Defaults to `null` — zero overhead until installed. |
66
- | `getContextInjector()` | function | Get current injector (returns `null` when none installed) |
67
- | `replaceContextInjector(ci)` | function | Install a custom injector (e.g. with OTel spans) |
68
- | `resetContextInjector()` | function | Reset injector back to `null` (disables instrumentation) |
69
-
70
- ## Types
71
-
72
- ```ts
73
- interface Logger {
74
- info(msg: string, ...args: unknown[]): void
75
- warn(msg: string, ...args: unknown[]): void
76
- error(msg: string, ...args: unknown[]): void
77
- debug(msg: string, ...args: unknown[]): void
78
- topic?: (name?: string) => Logger
79
- }
80
-
81
- interface Key<T> { readonly _id: number; readonly _name: string }
82
- interface Cached<T> { readonly _id: number; readonly _name: string; readonly _fn: (ctx: EventContext) => T }
83
- type Accessor<T> = Key<T> | Cached<T>
84
- interface SlotMarker<T> {} // phantom type marker
85
-
86
- type EventKind<S extends Record<string, SlotMarker<any>>> = {
87
- readonly name: string
88
- readonly keys: { [K in keyof S]: S[K] extends SlotMarker<infer V> ? Key<V> : never }
89
- readonly _entries: Array<[string, Key<unknown>]> // internal, used by ctx.seed()
90
- }
91
- type EventKindSeeds<K> = // maps SlotMarker<V> → V for each property
92
- ```
@@ -1,213 +0,0 @@
1
- # Primitives — @wooksjs/event-core
2
-
3
- > `key`, `cached`, `cachedBy`, `slot`, `defineEventKind` — the building blocks for typed per-event state.
4
-
5
- ## Concepts
6
-
7
- All state lives in `EventContext` — a flat `Map<number, unknown>`. Primitives give you typed accessors into that map:
8
-
9
- - **`key<T>`** — a writable slot. You `set` and `get` values explicitly.
10
- - **`cached<T>`** — a read-only slot. Computed lazily on first `get`, cached for the event lifetime.
11
- - **`cachedBy<K,V>`** — like `cached`, but parameterized. One cached result per unique key.
12
- - **`slot<T>`** / **`defineEventKind`** — group multiple keys into a typed seed bundle that can be seeded into a context in one call.
13
-
14
- All primitives are defined at module level (not per request). They produce lightweight descriptors with auto-incremented numeric IDs.
15
-
16
- ## API Reference
17
-
18
- ### `key<T>(name: string): Key<T>`
19
-
20
- Creates a writable typed slot. The `name` is for debugging only — lookup is by numeric ID.
21
-
22
- ```ts
23
- import { key, EventContext } from '@wooksjs/event-core'
24
-
25
- const userId = key<string>('userId')
26
-
27
- const ctx = new EventContext({ logger })
28
- ctx.set(userId, 'u-123')
29
- ctx.get(userId) // 'u-123' (typed as string)
30
- ```
31
-
32
- Throws `Key "name" is not set` if `get` is called before `set`.
33
-
34
- Supports `undefined`, `null`, `0`, `''`, `false` — all falsy values are stored correctly.
35
-
36
- ### `cached<T>(fn: (ctx: EventContext) => T): Cached<T>`
37
-
38
- Creates a lazy computed slot. The function runs on first `ctx.get()` and the result is cached.
39
-
40
- ```ts
41
- import { cached, key } from '@wooksjs/event-core'
42
-
43
- const url = key<string>('url')
44
- const parsedQuery = cached((ctx) => {
45
- return Object.fromEntries(new URL(ctx.get(url)).searchParams)
46
- })
47
-
48
- // First access: computes and caches
49
- ctx.get(parsedQuery)
50
- // Second access: returns cached result, fn never runs again
51
- ctx.get(parsedQuery)
52
- ```
53
-
54
- **Async:** The Promise itself is cached — concurrent access deduplicates:
55
-
56
- ```ts
57
- const rawBody = cached(async (ctx) => {
58
- const chunks: Buffer[] = []
59
- for await (const chunk of ctx.get(reqStream)) chunks.push(chunk)
60
- return Buffer.concat(chunks)
61
- })
62
-
63
- // Two concurrent calls → one stream read, one Promise
64
- const [a, b] = await Promise.all([ctx.get(rawBody), ctx.get(rawBody)])
65
- a === b // true
66
- ```
67
-
68
- **Errors** are cached too — a failed computation throws the same error on subsequent access without re-executing.
69
-
70
- **Circular dependencies** are detected and throw immediately: `Circular dependency detected for "name"`.
71
-
72
- ### `cachedBy<K, V>(fn: (key: K, ctx: EventContext) => V): (key: K, ctx?: EventContext) => V`
73
-
74
- Like `cached`, but parameterized. Maintains a `Map<K, V>` per context — each unique key computes once.
75
-
76
- ```ts
77
- import { cachedBy } from '@wooksjs/event-core'
78
-
79
- const getCookie = cachedBy((name: string, ctx) => {
80
- const header = ctx.get(rawHeaders)['cookie'] ?? ''
81
- const match = header.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`))
82
- return match?.[1]
83
- })
84
-
85
- getCookie('session') // extracts + caches
86
- getCookie('session') // cache hit
87
- getCookie('theme') // extracts + caches (different key)
88
- ```
89
-
90
- The returned function accepts an optional `ctx` parameter to skip `AsyncLocalStorage` lookup:
91
-
92
- ```ts
93
- const ctx = current()
94
- getCookie('session', ctx) // explicit context, no ALS overhead
95
- ```
96
-
97
- ### `slot<T>(): SlotMarker<T>`
98
-
99
- A phantom type marker used in `defineEventKind` schemas. Has no runtime behavior — it only carries the type `T`.
100
-
101
- ```ts
102
- import { slot } from '@wooksjs/event-core'
103
-
104
- const schema = {
105
- method: slot<string>(),
106
- path: slot<string>(),
107
- req: slot<IncomingMessage>(),
108
- }
109
- ```
110
-
111
- ### `defineEventKind<S>(name: string, schema: S): EventKind<S>`
112
-
113
- Groups slots into a named kind. Creates a `Key<T>` for each slot in the schema, namespaced under the kind name.
114
-
115
- ```ts
116
- import { defineEventKind, slot } from '@wooksjs/event-core'
117
-
118
- const httpKind = defineEventKind('http', {
119
- method: slot<string>(),
120
- path: slot<string>(),
121
- rawHeaders: slot<Record<string, string>>(),
122
- })
123
-
124
- // httpKind.name === 'http'
125
- // httpKind.keys.method is Key<string> with _name 'http.method'
126
- // httpKind.keys.path is Key<string> with _name 'http.path'
127
- ```
128
-
129
- Seed into a context with `ctx.seed()`:
130
-
131
- ```ts
132
- ctx.seed(httpKind, {
133
- method: 'POST',
134
- path: '/api/users',
135
- rawHeaders: { 'content-type': 'application/json' },
136
- })
137
-
138
- ctx.get(httpKind.keys.method) // 'POST'
139
- ```
140
-
141
- Multiple kinds can be layered via **parent-linked contexts** instead of seeding everything into one context:
142
-
143
- ```ts
144
- const parentCtx = new EventContext({ logger })
145
- parentCtx.seed(httpKind, { method: 'POST', path: '/webhook', rawHeaders: {} })
146
-
147
- const childCtx = new EventContext({ logger, parent: parentCtx })
148
- childCtx.seed(workflowKind, { triggerId: 'deploy-42', payload: { env: 'prod' } })
149
-
150
- // Child sees both its own and parent data via the parent chain
151
- childCtx.get(httpKind.keys.method) // 'POST' (from parent)
152
- childCtx.get(workflowKind.keys.triggerId) // 'deploy-42' (local)
153
- ```
154
-
155
- ## Common Patterns
156
-
157
- ### Pattern: Derived computation chain
158
-
159
- Cached values can depend on keys and on other cached values. The dependency graph is resolved lazily.
160
-
161
- ```ts
162
- const base = key<number>('base')
163
- const doubled = cached((ctx) => ctx.get(base) * 2)
164
- const quadrupled = cached((ctx) => ctx.get(doubled) * 2)
165
-
166
- ctx.set(base, 5)
167
- ctx.get(quadrupled) // 20 — computes doubled (10) then quadrupled (20)
168
- ```
169
-
170
- ### Pattern: Library extension via cached slots
171
-
172
- Libraries export `cached` slots for other libraries to depend on:
173
-
174
- ```ts
175
- // http-context library
176
- export const rawBody = cached(async (ctx) => readStream(ctx.get(httpKind.keys.req)))
177
-
178
- // body-parser library (depends on http-context)
179
- import { rawBody } from 'http-context'
180
-
181
- const parsedBody = cached(async (ctx) => {
182
- const buf = await ctx.get(rawBody)
183
- return JSON.parse(buf.toString())
184
- })
185
- ```
186
-
187
- ### Pattern: Sparse access with cachedBy
188
-
189
- When the data source is large but your app reads few entries:
190
-
191
- ```ts
192
- // Parses only requested cookies, not all 40
193
- const getCookie = cachedBy((name: string, ctx) => {
194
- const header = ctx.get(rawHeaders)['cookie'] ?? ''
195
- const match = header.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`))
196
- return match?.[1]
197
- })
198
- ```
199
-
200
- ## Best Practices
201
-
202
- - Define keys/cached/cachedBy at **module level**, not inside handlers — they're descriptors, not values
203
- - Use `cached` for anything computed from request data — it guarantees single computation
204
- - Use `cachedBy` when computation varies by a parameter (cookie name, header name, etc.)
205
- - Async `cached` values cache the Promise — safe for concurrent access
206
- - Errors from `cached` are also cached — a failing computation won't retry
207
-
208
- ## Gotchas
209
-
210
- - `key` throws on `get` if never `set` — use `ctx.has(k)` to check first
211
- - `cached` functions receive `ctx` as parameter — don't call `current()` inside them, use the provided `ctx`
212
- - Circular `cached` dependencies throw immediately — the cycle is detected at runtime
213
- - `cachedBy` uses a `Map` — keys are compared by identity (`===`), not deep equality