@wooksjs/event-wf 0.7.7 → 0.7.9

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.
@@ -1,449 +0,0 @@
1
- # Event Context Management — @wooksjs/event-core
2
-
3
- > Core machinery behind the Wooks framework: `EventContext`, `key`/`cached`/`cachedBy` primitives, `defineWook` composable factory, `defineEventKind`/`slot` for structured seeding, `run`/`current` for async propagation. This document is domain-agnostic — it applies equally to HTTP, CLI, workflow, and any custom event adapter built on Wooks.
4
-
5
- ## Mental Model
6
-
7
- Every event in Wooks (an HTTP request, a CLI invocation, a workflow step) 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
- **Composable functions** (the `use*()` pattern) are the primary way to interact with the context. Each composable reads from or writes to typed slots via `key()` and `cached()`. Values are **computed lazily** — only when first accessed — and then **cached** in the context for the lifetime of the event.
10
-
11
- ```
12
- Event arrives
13
- → new EventContext(options)
14
- → ctx.seed(kind, seeds)
15
- → run(ctx, handler)
16
- → handler calls composables
17
- → composables call current() to get the EventContext
18
- → ctx.get(key) / ctx.get(cached) to read slots
19
- → cached() computes on first access, returns cached on subsequent calls
20
- ```
21
-
22
- This architecture means:
23
-
24
- - No global mutable state — each event is fully isolated
25
- - No parameter drilling — composables access context from anywhere in the call chain
26
- - No wasted computation — if a handler never reads cookies, they're never parsed
27
- - Composables can call other composables — the caching ensures no redundant work
28
-
29
- ## Installation
30
-
31
- ```bash
32
- npm install @wooksjs/event-core
33
- ```
34
-
35
- Note: You typically don't install `event-core` directly. It's a peer dependency of adapters like `@wooksjs/event-http`, `@wooksjs/event-cli`, etc. But you import from it when creating custom composables.
36
-
37
- ## Primitives
38
-
39
- All state lives in `EventContext` — a flat `Map<number, unknown>`. Primitives give you typed accessors into that map.
40
-
41
- ### `key<T>(name: string): Key<T>`
42
-
43
- Creates a writable typed slot. The `name` is for debugging only — lookup is by numeric ID.
44
-
45
- ```ts
46
- import { key } from '@wooksjs/event-core'
47
-
48
- const userId = key<string>('userId')
49
-
50
- // In a context:
51
- ctx.set(userId, 'u-123')
52
- ctx.get(userId) // 'u-123' (typed as string)
53
- ```
54
-
55
- Throws `Key "name" is not set` if `get` is called before `set`. Supports `undefined`, `null`, `0`, `''`, `false` — all falsy values are stored correctly.
56
-
57
- ### `cached<T>(fn: (ctx: EventContext) => T): Cached<T>`
58
-
59
- Creates a lazy computed slot. The function runs on first `ctx.get()` and the result is cached.
60
-
61
- ```ts
62
- import { cached, key } from '@wooksjs/event-core'
63
-
64
- const url = key<string>('url')
65
- const parsedQuery = cached((ctx) => {
66
- return Object.fromEntries(new URL(ctx.get(url)).searchParams)
67
- })
68
-
69
- // First access: computes and caches
70
- ctx.get(parsedQuery)
71
- // Second access: returns cached result, fn never runs again
72
- ctx.get(parsedQuery)
73
- ```
74
-
75
- **Async:** The Promise itself is cached — concurrent access deduplicates.
76
-
77
- **Errors** are cached too — a failed computation throws the same error on subsequent access without re-executing.
78
-
79
- **Circular dependencies** are detected and throw immediately.
80
-
81
- ### `cachedBy<K, V>(fn: (key: K, ctx: EventContext) => V): (key: K, ctx?: EventContext) => V`
82
-
83
- Like `cached`, but parameterized. Maintains a `Map<K, V>` per context — each unique key computes once.
84
-
85
- ```ts
86
- import { cachedBy } from '@wooksjs/event-core'
87
-
88
- const getCookie = cachedBy((name: string, ctx) => {
89
- const header = ctx.get(rawHeaders)['cookie'] ?? ''
90
- const match = header.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`))
91
- return match?.[1]
92
- })
93
-
94
- getCookie('session') // extracts + caches
95
- getCookie('session') // cache hit
96
- getCookie('theme') // extracts + caches (different key)
97
- ```
98
-
99
- ### `slot<T>()` and `defineEventKind(name, schema)`
100
-
101
- Group multiple keys into a typed seed bundle:
102
-
103
- ```ts
104
- import { defineEventKind, slot } from '@wooksjs/event-core'
105
-
106
- const httpKind = defineEventKind('http', {
107
- method: slot<string>(),
108
- path: slot<string>(),
109
- rawHeaders: slot<Record<string, string>>(),
110
- })
111
-
112
- // httpKind.keys.method is Key<string>
113
- // httpKind.keys.path is Key<string>
114
- ```
115
-
116
- Seed into a context:
117
-
118
- ```ts
119
- ctx.seed(httpKind, {
120
- method: 'POST',
121
- path: '/api/users',
122
- rawHeaders: { 'content-type': 'application/json' },
123
- })
124
-
125
- ctx.get(httpKind.keys.method) // 'POST'
126
- ```
127
-
128
- Multiple kinds can be layered via **parent-linked contexts** instead of seeding everything into one context:
129
-
130
- ```ts
131
- const parentCtx = new EventContext({ logger })
132
- parentCtx.seed(httpKind, { method: 'POST', path: '/webhook', rawHeaders: {} })
133
-
134
- const childCtx = new EventContext({ logger, parent: parentCtx })
135
- childCtx.seed(workflowKind, { triggerId: 'deploy-42' })
136
-
137
- // Child sees both its own and parent data via the parent chain
138
- childCtx.get(httpKind.keys.method) // 'POST' (from parent)
139
- childCtx.get(workflowKind.keys.triggerId) // 'deploy-42' (local)
140
- ```
141
-
142
- ## EventContext
143
-
144
- ```ts
145
- import { EventContext } from '@wooksjs/event-core'
146
-
147
- const ctx = new EventContext({ logger })
148
- ```
149
-
150
- **Constructor options:**
151
-
152
- ```ts
153
- interface EventContextOptions {
154
- logger: Logger
155
- parent?: EventContext
156
- }
157
- ```
158
-
159
- 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.):
160
-
161
- ```ts
162
- const parentCtx = new EventContext({ logger })
163
- parentCtx.seed(httpKind, { method: 'GET', path: '/api' })
164
-
165
- const childCtx = new EventContext({ logger, parent: parentCtx })
166
- childCtx.seed(workflowKind, { triggerId: 'deploy-42' })
167
-
168
- // child can see its own data AND parent data
169
- childCtx.get(httpKind.keys.method) // 'GET' (found in parent)
170
- childCtx.get(workflowKind.keys.triggerId) // 'deploy-42' (found locally)
171
- ```
172
-
173
- **Methods:**
174
-
175
- - `ctx.get(accessor)` — Get by key or cached. Traverses parent chain.
176
- - `ctx.set(key, value)` — Set a value. Writes to first context in chain where key exists, or locally if new.
177
- - `ctx.has(accessor)` — Check if set. Traverses parent chain.
178
- - `ctx.getOwn(accessor)` — Local only `get`. Does not traverse parents.
179
- - `ctx.setOwn(key, value)` — Local only `set`. Always writes locally.
180
- - `ctx.hasOwn(accessor)` — Local only `has`.
181
- - `ctx.seed(kind, seeds[, fn])` — Populate from an EventKind schema.
182
- - `ctx.logger` — The Logger instance.
183
-
184
- ## Runtime
185
-
186
- ### `run(ctx, fn)`
187
-
188
- Execute `fn` within the `AsyncLocalStorage` scope of `ctx`:
189
-
190
- ```ts
191
- import { run, current, EventContext } from '@wooksjs/event-core'
192
-
193
- const ctx = new EventContext({ logger })
194
- const result = run(ctx, () => {
195
- current() === ctx // true
196
- return 'hello'
197
- })
198
- ```
199
-
200
- ### `current()`
201
-
202
- Get the active `EventContext`. Throws if called outside a `run` scope.
203
-
204
- ### `tryGetCurrent()`
205
-
206
- Like `current()` but returns `undefined` instead of throwing.
207
-
208
- ### `createEventContext(options, fn)`
209
-
210
- Convenience: creates a context and runs `fn` inside `AsyncLocalStorage`:
211
-
212
- ```ts
213
- import { createEventContext } from '@wooksjs/event-core'
214
-
215
- createEventContext({ logger }, () => {
216
- const ctx = current()
217
- // ...
218
- })
219
-
220
- // With kind + seeds:
221
- createEventContext({ logger }, httpKind, { method: 'GET', path: '/' }, () => {
222
- current().get(httpKind.keys.method) // 'GET'
223
- })
224
- ```
225
-
226
- ### `useLogger(ctx?)`
227
-
228
- Shorthand for `(ctx ?? current()).logger`:
229
-
230
- ```ts
231
- import { useLogger } from '@wooksjs/event-core'
232
-
233
- const logger = useLogger()
234
- logger.info('processing event')
235
- ```
236
-
237
- ## Creating Custom Composables
238
-
239
- ### `defineWook<T>(factory: (ctx: EventContext) => T): (ctx?: EventContext) => T`
240
-
241
- Creates a composable with per-event caching. The factory runs once per `EventContext`, and the result is cached:
242
-
243
- ```ts
244
- import { defineWook, key } from '@wooksjs/event-core'
245
-
246
- const userIdKey = key<string>('auth.userId')
247
-
248
- const useCurrentUser = defineWook((ctx) => {
249
- return {
250
- getUserId: () => ctx.get(userIdKey),
251
- isAdmin: () => ctx.get(userIdKey) === 'admin',
252
- }
253
- })
254
-
255
- // In a handler:
256
- const { getUserId, isAdmin } = useCurrentUser()
257
- ```
258
-
259
- **Caching guarantee:** The factory runs exactly once per event context. Subsequent calls return the same object:
260
-
261
- ```ts
262
- const a = useCurrentUser()
263
- const b = useCurrentUser()
264
- a === b // true — same object reference
265
- ```
266
-
267
- **Optional explicit context:** Pass `ctx` to skip the `AsyncLocalStorage` lookup:
268
-
269
- ```ts
270
- const ctx = current()
271
- const { method } = useRequest(ctx)
272
- const { parseBody } = useBody(ctx)
273
- ```
274
-
275
- ### Pattern: Composable with lazy cached properties
276
-
277
- Combine `defineWook` with `cached` for deferred computation:
278
-
279
- ```ts
280
- import { defineWook, cached } from '@wooksjs/event-core'
281
-
282
- const parsedBody = cached(async (ctx) => {
283
- const buf = await ctx.get(rawBody)
284
- return JSON.parse(buf.toString())
285
- })
286
-
287
- const useBody = defineWook((ctx) => ({
288
- parseBody: () => ctx.get(parsedBody),
289
- }))
290
-
291
- // In handler:
292
- const { parseBody } = useBody()
293
- const body = await parseBody() // parses once
294
- const again = await parseBody() // cache hit, same object
295
- ```
296
-
297
- ### Pattern: Mutable state with key()
298
-
299
- Use `key()` when you need a read/write slot that handlers can modify during execution:
300
-
301
- ```ts
302
- import { key, defineWook } from '@wooksjs/event-core'
303
-
304
- const errorsKey = key<string[]>('validation.errors')
305
-
306
- export const useValidation = defineWook((ctx) => {
307
- ctx.set(errorsKey, [])
308
-
309
- return {
310
- addError: (msg: string) => ctx.get(errorsKey).push(msg),
311
- getErrors: () => ctx.get(errorsKey),
312
- hasErrors: () => ctx.get(errorsKey).length > 0,
313
- }
314
- })
315
- ```
316
-
317
- ### Pattern: Composable calling another composable
318
-
319
- Composables can call other composables inside their factory. Pass `ctx` to avoid redundant `AsyncLocalStorage` lookups:
320
-
321
- ```ts
322
- const useCurrentUser = defineWook((ctx) => {
323
- const { basicCredentials } = useAuthorization(ctx)
324
- const username = basicCredentials()?.username
325
- return {
326
- username,
327
- profile: async () => (username ? await db.findUser(username) : null),
328
- }
329
- })
330
- ```
331
-
332
- ### Pattern: Plain function for single-value access
333
-
334
- `defineWook` adds caching overhead. For trivial single-value access, use a plain function:
335
-
336
- ```ts
337
- // Better — one Map.get, no caching layer
338
- function useMethod(ctx?: EventContext): string {
339
- return (ctx ?? current()).get(http.keys.method) ?? 'GET'
340
- }
341
- ```
342
-
343
- ## Built-in Composables
344
-
345
- These ship with `@wooksjs/event-core` and work in any adapter:
346
-
347
- ### `useRouteParams<T>(ctx?)`
348
-
349
- Access route parameters set by the router:
350
-
351
- ```ts
352
- import { useRouteParams } from '@wooksjs/event-core'
353
-
354
- const { params, get } = useRouteParams<{ id: string }>()
355
- console.log(get('id')) // '123'
356
- console.log(params) // { id: '123' }
357
- ```
358
-
359
- ### `useEventId(ctx?)`
360
-
361
- Returns a lazy UUID for the current event:
362
-
363
- ```ts
364
- import { useEventId } from '@wooksjs/event-core'
365
-
366
- const { getId } = useEventId()
367
- console.log(getId()) // '550e8400-e29b-41d4-a716-446655440000'
368
- // Second call returns the same ID
369
- ```
370
-
371
- ### `useLogger(ctx?)`
372
-
373
- Returns the logger from the current event context:
374
-
375
- ```ts
376
- import { useLogger } from '@wooksjs/event-core'
377
-
378
- const logger = useLogger()
379
- logger.info('processing event')
380
- logger.warn('something unusual')
381
- logger.error('something failed')
382
- ```
383
-
384
- ## Creating an Event Context (for adapter authors)
385
-
386
- Each adapter exports a **context factory** that hardcodes the event kind and delegates to `createEventContext`. The factory signature mirrors `createEventContext` but omits the `kind` parameter:
387
-
388
- ```ts
389
- import { createEventContext, defineEventKind, slot } from '@wooksjs/event-core'
390
- import type { EventContextOptions, EventKindSeeds } from '@wooksjs/event-core'
391
-
392
- const myKind = defineEventKind('my-event', {
393
- data: slot<unknown>(),
394
- source: slot<string>(),
395
- })
396
-
397
- // Context factory — kind is hardcoded inside
398
- export function createMyEventContext<R>(
399
- options: EventContextOptions,
400
- seeds: EventKindSeeds<typeof myKind>,
401
- fn: () => R,
402
- ): R {
403
- return createEventContext(options, myKind, seeds, fn)
404
- }
405
-
406
- // Usage in the adapter
407
- function handleEvent(data: unknown, source: string, handler: () => unknown) {
408
- return createMyEventContext({ logger: console }, { data, source }, handler)
409
- }
410
- ```
411
-
412
- All built-in adapters follow this pattern: `createHttpContext`, `createCliContext`, `createWsConnectionContext`, `createWsMessageContext`, `createWfContext`, `resumeWfContext`.
413
-
414
- ### Parent context (nested events)
415
-
416
- Create child contexts by passing `parent` in the options. Each child sees its own data plus everything in the parent chain:
417
-
418
- ```ts
419
- createEventContext({ logger }, httpKind, httpSeeds, () => {
420
- const parentCtx = current()
421
-
422
- // Child context linked to the HTTP parent
423
- createEventContext({ logger, parent: parentCtx }, workflowKind, wfSeeds, () => {
424
- // Both HTTP and workflow composables work
425
- const { method } = useRequest() // found via parent chain
426
- const { ctx } = useWfState() // found locally
427
- })
428
- })
429
- ```
430
-
431
- ## Best Practices
432
-
433
- - **Define keys/cached/cachedBy at module level** — they're descriptors, not values. Don't create them inside handlers.
434
- - **Use `cached` for anything computed from event data** — it guarantees single computation and deduplicates async work.
435
- - **Use `defineWook` when the factory returns an object with multiple properties or methods** — use plain functions for trivial single-value access.
436
- - **Always accept optional `ctx` parameter** in composables for performance — pass `ctx` explicitly when calling multiple composables in sequence.
437
- - **Prefer parent-linked child contexts** over seeding multiple kinds into one context — keeps each layer's data isolated while still accessible.
438
- - **Keep computed values idempotent** — the factory passed to `cached()` runs exactly once and is cached permanently for the event.
439
-
440
- ## Gotchas
441
-
442
- - **Composables must be called within an event context** — Calling any composable outside a `run()` scope throws `[Wooks] No active event context`.
443
- - **Context is async-chain scoped** — The context is bound to the async execution chain via `AsyncLocalStorage`. If you break the chain (e.g., `setTimeout` with a separate callback), the context may be lost. Use `AsyncResource.bind()` if needed.
444
- - **`cached()` caches permanently for the event** — If computation fails, the error is cached too. No retry.
445
- - **`key()` throws on `get` if never `set`** — use `ctx.has(k)` to check first.
446
- - **`cached()` functions receive `ctx` as parameter** — don't call `current()` inside them, use the provided `ctx`.
447
- - **Circular `cached` dependencies throw immediately** — the cycle is detected at runtime.
448
- - **`set()` traverses the parent chain** — it writes to the first context where the key exists. Use `setOwn()` to shadow a parent value locally.
449
- - **Don't hold references across events** — Values from the context are scoped to one event. Don't cache them in module-level variables.