@wooksjs/event-core 0.6.5 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,91 @@
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.) |
66
+ | `getContextInjector()` | function | Get current injector |
67
+ | `replaceContextInjector(ci)` | function | Replace injector (e.g. with OTel spans) |
68
+
69
+ ## Types
70
+
71
+ ```ts
72
+ interface Logger {
73
+ info(msg: string, ...args: unknown[]): void
74
+ warn(msg: string, ...args: unknown[]): void
75
+ error(msg: string, ...args: unknown[]): void
76
+ debug(msg: string, ...args: unknown[]): void
77
+ topic?: (name?: string) => Logger
78
+ }
79
+
80
+ interface Key<T> { readonly _id: number; readonly _name: string }
81
+ interface Cached<T> { readonly _id: number; readonly _name: string; readonly _fn: (ctx: EventContext) => T }
82
+ type Accessor<T> = Key<T> | Cached<T>
83
+ interface SlotMarker<T> {} // phantom type marker
84
+
85
+ type EventKind<S extends Record<string, SlotMarker<any>>> = {
86
+ readonly name: string
87
+ readonly keys: { [K in keyof S]: S[K] extends SlotMarker<infer V> ? Key<V> : never }
88
+ readonly _entries: Array<[string, Key<unknown>]> // internal, used by ctx.seed()
89
+ }
90
+ type EventKindSeeds<K> = // maps SlotMarker<V> → V for each property
91
+ ```
@@ -0,0 +1,213 @@
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