@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 +6 -14
- package/dist/index.d.ts +12 -4
- package/dist/index.mjs +6 -14
- package/package.json +3 -9
- package/scripts/setup-skills.js +0 -78
- package/skills/wooksjs-event-core/.gitkeep +0 -0
- package/skills/wooksjs-event-core/SKILL.md +0 -51
- package/skills/wooksjs-event-core/composables.md +0 -200
- package/skills/wooksjs-event-core/context.md +0 -288
- package/skills/wooksjs-event-core/core.md +0 -92
- package/skills/wooksjs-event-core/primitives.md +0 -213
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.
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
|
12
|
-
|
|
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
|
-
*
|
|
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(
|
|
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.
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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.
|
|
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
|
}
|
package/scripts/setup-skills.js
DELETED
|
@@ -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
|