@wooksjs/event-cli 0.6.2 → 0.6.4

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,562 @@
1
+ # Event Context Management — @wooksjs/event-core
2
+
3
+ > Core machinery behind the Wooks framework: AsyncLocalStorage-based event context, the context store API, creating custom composables with lazy evaluation and caching. 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 **context store** — an isolated plain object that lives for the duration of that event. The store is held in Node.js `AsyncLocalStorage`, so any function called within the event's async chain can access it without parameter passing.
8
+
9
+ **Composable functions** (the `use*()` pattern) are the primary way to interact with the store. Each composable reads from or writes to a named section of the store. Values are **computed lazily** — only when first accessed — and then **cached** in the store for the lifetime of the event.
10
+
11
+ ```
12
+ Event arrives
13
+ → createAsyncEventContext(storeData)
14
+ → AsyncLocalStorage.run(store, handler)
15
+ → handler calls composables
16
+ → composables call useAsyncEventContext()
17
+ → reads/writes the context store via store() helpers
18
+ → init() computes on first access, returns cached on subsequent calls
19
+ ```
20
+
21
+ This architecture means:
22
+ - No global mutable state — each event is fully isolated
23
+ - No parameter drilling — composables access context from anywhere in the call chain
24
+ - No wasted computation — if a handler never reads cookies, they're never parsed
25
+ - Composables can call other composables — the caching ensures no redundant work
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ npm install @wooksjs/event-core
31
+ ```
32
+
33
+ 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.
34
+
35
+ ## The Context Store
36
+
37
+ ### Structure
38
+
39
+ The context store is a typed object. At its base level (from `event-core`), it has:
40
+
41
+ ```ts
42
+ interface TGenericContextStore<CustomEventType = TEmpty> {
43
+ event: CustomEventType & TGenericEvent // event-specific data + { type, logger?, id? }
44
+ options: TEventOptions // logger configuration
45
+ parentCtx?: TGenericContextStore // parent event context (for nested events)
46
+ routeParams?: Record<string, string | string[]> // route parameters
47
+ }
48
+ ```
49
+
50
+ Each adapter extends this with its own sections. For example, an HTTP adapter adds `request`, `cookies`, `status`, etc. **Your custom composables extend it further** by declaring additional typed sections.
51
+
52
+ ### Two-level nesting
53
+
54
+ The store uses a **two-level key structure**: `store(section).method(key)`.
55
+
56
+ - **Level 1 (section)**: A named top-level section like `'event'`, `'request'`, `'cookies'`, or your custom `'myFeature'`.
57
+ - **Level 2 (key)**: A property within that section, like `'id'`, `'rawBody'`, `'parsedToken'`.
58
+
59
+ This design keeps sections isolated and typed independently.
60
+
61
+ ## Context Store API
62
+
63
+ ### Accessing the context
64
+
65
+ ```ts
66
+ import { useAsyncEventContext } from '@wooksjs/event-core'
67
+
68
+ const { store, getCtx, getStore, setStore, hasParentCtx, getParentCtx } = useAsyncEventContext()
69
+ ```
70
+
71
+ Or in an adapter wrapper (typical usage):
72
+
73
+ ```ts
74
+ // Each adapter provides its own typed wrapper, e.g.:
75
+ const { store, getCtx } = useMyAdapterContext()
76
+ ```
77
+
78
+ ### `store(section)` — Section accessor
79
+
80
+ The primary API. Returns an object with utility methods for one section of the store:
81
+
82
+ ```ts
83
+ const section = store('mySection')
84
+ // section.init, section.get, section.set, section.has, section.del
85
+ // section.hook, section.entries, section.clear, section.value
86
+ ```
87
+
88
+ ---
89
+
90
+ ### `init(key, getter)` — Lazy initialize (most important method)
91
+
92
+ **This is the core pattern of the entire framework.** If the value for `key` doesn't exist yet, `init` calls `getter()`, stores the result, and returns it. On subsequent calls, it returns the cached value without calling `getter` again.
93
+
94
+ ```ts
95
+ const { init } = store('mySection')
96
+
97
+ // First call → getter runs, result is stored
98
+ const value = init('expensiveResult', () => computeSomethingExpensive())
99
+
100
+ // Second call → cached value returned, getter is NOT called
101
+ const sameValue = init('expensiveResult', () => computeSomethingExpensive())
102
+ ```
103
+
104
+ `init()` is what makes composables lazy. Instead of computing at composable creation time, you wrap the computation in `init()` and return a function that calls it:
105
+
106
+ ```ts
107
+ // CORRECT: lazy — computation deferred until called
108
+ function useMyComposable() {
109
+ const { init } = store('mySection')
110
+ const getExpensiveValue = () => init('expensive', () => compute())
111
+ return { getExpensiveValue }
112
+ }
113
+
114
+ // WRONG: eager — computation runs immediately when composable is called
115
+ function useMyComposable() {
116
+ const value = compute() // runs even if never needed
117
+ return { value }
118
+ }
119
+ ```
120
+
121
+ ---
122
+
123
+ ### `get(key)` — Read a value
124
+
125
+ Returns the stored value or `undefined` if not set:
126
+
127
+ ```ts
128
+ const { get } = store('mySection')
129
+ const value = get('someKey') // T | undefined
130
+ ```
131
+
132
+ ---
133
+
134
+ ### `set(key, value)` — Write a value
135
+
136
+ Stores a value directly (no lazy evaluation):
137
+
138
+ ```ts
139
+ const { set } = store('mySection')
140
+ set('limit', 1024)
141
+ ```
142
+
143
+ ---
144
+
145
+ ### `has(key)` — Check if a value exists
146
+
147
+ ```ts
148
+ const { has } = store('mySection')
149
+ if (!has('computed')) {
150
+ // not yet initialized
151
+ }
152
+ ```
153
+
154
+ ---
155
+
156
+ ### `del(key)` — Remove a value
157
+
158
+ Sets the value to `undefined`:
159
+
160
+ ```ts
161
+ const { del } = store('mySection')
162
+ del('cached')
163
+ ```
164
+
165
+ ---
166
+
167
+ ### `hook(key)` — Reactive accessor
168
+
169
+ Creates an object with a `value` property backed by getter/setter (using `Object.defineProperty`). Useful when you want to pass a reference that other code can read or write reactively:
170
+
171
+ ```ts
172
+ const { hook } = store('mySection')
173
+ const myHook = hook('status')
174
+
175
+ // Write
176
+ myHook.value = 'active'
177
+
178
+ // Read
179
+ console.log(myHook.value) // 'active'
180
+
181
+ // Check if explicitly set
182
+ console.log(myHook.isDefined) // true
183
+ ```
184
+
185
+ Hooks are the mechanism behind `useStatus()`, `useSetCookie()`, `useSetHeader()`, and other hookable APIs in the framework. They let you return a reactive reference from a composable that external code (like framework internals) can observe.
186
+
187
+ ---
188
+
189
+ ### `entries()` — List all key-value pairs in a section
190
+
191
+ ```ts
192
+ const { entries } = store('mySection')
193
+ const all = entries() // [['key1', value1], ['key2', value2]]
194
+ ```
195
+
196
+ ---
197
+
198
+ ### `clear()` — Reset a section
199
+
200
+ Replaces the section with an empty object:
201
+
202
+ ```ts
203
+ const { clear } = store('mySection')
204
+ clear()
205
+ ```
206
+
207
+ ---
208
+
209
+ ### `store(section).value` — Direct section access
210
+
211
+ The `value` property on the section object is a hooked getter/setter for the **entire section object**:
212
+
213
+ ```ts
214
+ // Set the entire section
215
+ store('routeParams').value = { id: '123', slug: 'hello' }
216
+
217
+ // Read the entire section
218
+ const params = store('routeParams').value // { id: '123', slug: 'hello' }
219
+ ```
220
+
221
+ ### Top-level helpers
222
+
223
+ ```ts
224
+ const { getCtx, getStore, setStore, hasParentCtx, getParentCtx } = useAsyncEventContext()
225
+
226
+ getCtx() // entire context store object
227
+ getStore('event') // shortcut: reads a top-level section directly
228
+ setStore('event', value) // shortcut: writes a top-level section directly
229
+
230
+ hasParentCtx() // true if this event is nested inside another
231
+ getParentCtx() // access the parent context's helpers (throws if none)
232
+ ```
233
+
234
+ ## Creating Custom Composables
235
+
236
+ This is the main reason to understand `event-core`. Custom composables let you encapsulate any domain-specific logic — user fetching, validation, token parsing, feature flags, caching — using the same lazy-evaluated, cached, context-scoped pattern.
237
+
238
+ ### Step 1: Define your store section type
239
+
240
+ Declare a TypeScript interface for your composable's section of the store. All properties should be optional (they start as `undefined`):
241
+
242
+ ```ts
243
+ interface TUserStore {
244
+ user?: {
245
+ current?: { id: string; name: string; role: string } | null
246
+ isAdmin?: boolean
247
+ permissions?: string[]
248
+ }
249
+ }
250
+ ```
251
+
252
+ ### Step 2: Access the context with your type
253
+
254
+ Use the generic parameter on your adapter's context hook to extend the store type:
255
+
256
+ ```ts
257
+ import { useAsyncEventContext } from '@wooksjs/event-core'
258
+
259
+ function useUser() {
260
+ // The generic parameter merges your type with the base store
261
+ const { store } = useAsyncEventContext<TUserStore>()
262
+ const { init } = store('user')
263
+
264
+ // ...
265
+ }
266
+ ```
267
+
268
+ If you're working within an adapter (e.g., HTTP), use that adapter's typed context hook instead:
269
+
270
+ ```ts
271
+ // Inside an HTTP adapter context:
272
+ // const { store } = useHttpContext<TUserStore>()
273
+ //
274
+ // Inside a CLI adapter context:
275
+ // const { store } = useCliContext<TUserStore>()
276
+ ```
277
+
278
+ ### Step 3: Implement lazy-computed properties
279
+
280
+ Return functions that call `init()` internally. The computation runs only when the function is called, and the result is cached:
281
+
282
+ ```ts
283
+ function useUser() {
284
+ const { store } = useAsyncEventContext<TUserStore>()
285
+ const { init } = store('user')
286
+
287
+ const currentUser = () =>
288
+ init('current', () => {
289
+ // This runs ONCE, only when currentUser() is first called.
290
+ // Replace with your actual user-fetching logic:
291
+ const token = getTokenFromSomewhere()
292
+ if (!token) return null
293
+ return decodeAndVerify(token)
294
+ })
295
+
296
+ const isAdmin = () =>
297
+ init('isAdmin', () => {
298
+ // Calls currentUser() — if already cached, no extra work
299
+ return currentUser()?.role === 'admin'
300
+ })
301
+
302
+ const permissions = () =>
303
+ init('permissions', () => {
304
+ const user = currentUser()
305
+ if (!user) return []
306
+ return fetchPermissions(user.id)
307
+ })
308
+
309
+ return { currentUser, isAdmin, permissions }
310
+ }
311
+ ```
312
+
313
+ ### Step 4: Use in handlers
314
+
315
+ ```ts
316
+ // In any handler, anywhere in the call chain:
317
+ const { currentUser, isAdmin } = useUser()
318
+ const user = currentUser() // computed and cached
319
+ if (!isAdmin()) {
320
+ // isAdmin() reuses the cached user — no re-computation
321
+ throw new Error('Forbidden')
322
+ }
323
+ ```
324
+
325
+ ### Full Example: Validation composable
326
+
327
+ ```ts
328
+ interface TValidationStore {
329
+ validation?: {
330
+ errors?: Record<string, string>
331
+ isValid?: boolean
332
+ }
333
+ }
334
+
335
+ function useValidation() {
336
+ const { store } = useAsyncEventContext<TValidationStore>()
337
+ const { get, set, has } = store('validation')
338
+
339
+ function addError(field: string, message: string) {
340
+ const errors = get('errors') || {}
341
+ errors[field] = message
342
+ set('errors', errors)
343
+ set('isValid', false)
344
+ }
345
+
346
+ function isValid() {
347
+ // Default to true if no errors added
348
+ return !has('isValid') ? true : get('isValid')!
349
+ }
350
+
351
+ function getErrors() {
352
+ return get('errors') || {}
353
+ }
354
+
355
+ return { addError, isValid, getErrors }
356
+ }
357
+ ```
358
+
359
+ ### Full Example: Caching composable
360
+
361
+ ```ts
362
+ interface TCacheStore {
363
+ cache?: Record<string, unknown>
364
+ }
365
+
366
+ function useEventCache() {
367
+ const { store } = useAsyncEventContext<TCacheStore>()
368
+ const { init, get, set, has, del } = store('cache')
369
+
370
+ return {
371
+ // Lazy-cached: fetches once, returns cached on subsequent calls
372
+ cached: <T>(key: string, fetcher: () => T): T => init(key, fetcher) as T,
373
+ get: <T>(key: string) => get(key) as T | undefined,
374
+ set: <T>(key: string, value: T) => set(key, value),
375
+ has: (key: string) => has(key),
376
+ invalidate: (key: string) => del(key),
377
+ }
378
+ }
379
+
380
+ // Usage:
381
+ const cache = useEventCache()
382
+ const user = cache.cached('user:42', () => db.findUser(42))
383
+ // Second call returns cached — db.findUser is NOT called again
384
+ const sameUser = cache.cached('user:42', () => db.findUser(42))
385
+ ```
386
+
387
+ ### Full Example: Hookable composable
388
+
389
+ Use `hook()` when you need to return a reactive reference that framework code or other composables can read or write:
390
+
391
+ ```ts
392
+ interface TFeatureFlagStore {
393
+ flags?: {
394
+ darkMode?: boolean
395
+ betaFeatures?: boolean
396
+ }
397
+ }
398
+
399
+ function useDarkMode() {
400
+ const { store } = useAsyncEventContext<TFeatureFlagStore>()
401
+ return store('flags').hook('darkMode')
402
+ // Returns { value: boolean | undefined, isDefined: boolean }
403
+ }
404
+
405
+ // Usage:
406
+ const darkMode = useDarkMode()
407
+ darkMode.value = true // set
408
+ console.log(darkMode.value) // true
409
+ console.log(darkMode.isDefined) // true
410
+ ```
411
+
412
+ ## Built-in Composables (from event-core)
413
+
414
+ These ship with `@wooksjs/event-core` and work in any adapter:
415
+
416
+ ### `useEventId()`
417
+
418
+ Generates a UUID for the current event, lazily on first call:
419
+
420
+ ```ts
421
+ import { useEventId } from '@wooksjs/event-core'
422
+
423
+ const { getId } = useEventId()
424
+ console.log(getId()) // '550e8400-e29b-41d4-a716-446655440000'
425
+ // Second call returns the same ID
426
+ ```
427
+
428
+ ### `useRouteParams<T>()`
429
+
430
+ Access route parameters set by the router:
431
+
432
+ ```ts
433
+ import { useRouteParams } from '@wooksjs/event-core'
434
+
435
+ const { params, get } = useRouteParams<{ id: string }>()
436
+ console.log(get('id')) // '123'
437
+ console.log(params) // { id: '123' }
438
+ ```
439
+
440
+ ### `useEventLogger(topic?)`
441
+
442
+ Returns a logger scoped to the current event (tagged with the event ID):
443
+
444
+ ```ts
445
+ import { useEventLogger } from '@wooksjs/event-core'
446
+
447
+ const logger = useEventLogger('my-handler')
448
+ logger.log('processing event')
449
+ logger.warn('something unusual')
450
+ logger.error('something failed')
451
+ ```
452
+
453
+ ## Creating an Event Context (for adapter authors)
454
+
455
+ If you're building a custom adapter (not using the built-in HTTP/CLI ones), you create the context using `createAsyncEventContext`:
456
+
457
+ ```ts
458
+ import { createAsyncEventContext, useAsyncEventContext } from '@wooksjs/event-core'
459
+
460
+ // 1. Define your event data and store types
461
+ interface TMyEventData {
462
+ payload: unknown
463
+ source: string
464
+ }
465
+
466
+ interface TMyContextStore {
467
+ parsed?: { data?: unknown }
468
+ meta?: Record<string, unknown>
469
+ }
470
+
471
+ // 2. Create a context factory
472
+ function createMyContext(data: TMyEventData, options = {}) {
473
+ return createAsyncEventContext<TMyContextStore, TMyEventData>({
474
+ event: {
475
+ ...data,
476
+ type: 'MY_EVENT', // unique event type identifier
477
+ },
478
+ options,
479
+ })
480
+ }
481
+
482
+ // 3. Create a typed context accessor
483
+ function useMyContext() {
484
+ return useAsyncEventContext<TMyContextStore, TMyEventData>('MY_EVENT')
485
+ }
486
+
487
+ // 4. Use it
488
+ function handleEvent(data: TMyEventData) {
489
+ const runInContext = createMyContext(data)
490
+ runInContext(() => {
491
+ // Inside here, useMyContext() works
492
+ const { store } = useMyContext()
493
+ const { init } = store('parsed')
494
+ const parsed = init('data', () => JSON.parse(data.payload as string))
495
+ // ...
496
+ })
497
+ }
498
+ ```
499
+
500
+ ### Parent context (nested events)
501
+
502
+ If `createAsyncEventContext` is called inside an existing event context, the new context automatically gets a reference to the parent:
503
+
504
+ ```ts
505
+ const { hasParentCtx, getParentCtx } = useAsyncEventContext()
506
+ if (hasParentCtx()) {
507
+ const parent = getParentCtx()
508
+ const parentEvent = parent.getStore('event')
509
+ }
510
+ ```
511
+
512
+ ## `attachHook` — Low-level hook utility
513
+
514
+ For advanced use cases, `attachHook` lets you attach getter/setter hooks to any object property:
515
+
516
+ ```ts
517
+ import { attachHook } from '@wooksjs/event-core'
518
+
519
+ const obj = { name: 'status' }
520
+ attachHook(obj, {
521
+ get: () => store.get('value'),
522
+ set: (v) => store.set('value', v),
523
+ })
524
+
525
+ // Now obj.value is reactive:
526
+ obj.value = 42 // calls the setter
527
+ console.log(obj.value) // calls the getter
528
+
529
+ // Hook a custom property name:
530
+ attachHook(obj, { get: () => 'hello' }, 'greeting')
531
+ console.log(obj.greeting) // 'hello'
532
+ ```
533
+
534
+ ## Best Practices
535
+
536
+ - **Always use `init()` for computed values** — This is the single most important pattern. It ensures lazy evaluation and caching. Never compute eagerly when the composable is instantiated.
537
+
538
+ - **Return functions, not values** — Composables should return accessor functions (e.g., `currentUser()` not `currentUser`). This preserves laziness — the value is only computed when the returned function is called.
539
+
540
+ - **One composable per store section** — Each composable should own one named section of the store. This keeps the type system clean and prevents collisions.
541
+
542
+ - **Composables can (and should) call other composables** — This is the expected composition pattern. A `useUser()` composable can call `useAuth()` internally. Thanks to `init()` caching, there's zero redundant computation even when multiple composables read the same data.
543
+
544
+ - **Extend the store type with generics** — Always pass your store type as the generic parameter: `useAsyncEventContext<TMyStore>()`. This gives you full type safety and autocompletion for `store('mySection').init(...)`.
545
+
546
+ - **Keep computed values idempotent** — The getter passed to `init()` should always produce the same result for the same inputs. It runs exactly once and is cached permanently for the event.
547
+
548
+ - **Name sections semantically** — Use descriptive section names: `'user'`, `'validation'`, `'cache'`, not `'data1'` or `'temp'`.
549
+
550
+ ## Gotchas
551
+
552
+ - **Composables must be called within an event context** — Calling any composable outside a context scope (e.g., at module init time, or in an orphaned `setTimeout`) throws: `"Event context does not exist at this point."`.
553
+
554
+ - **Context is async-chain scoped** — The context is bound to the async execution chain via `AsyncLocalStorage`. If you break the chain (e.g., `setTimeout`, `setImmediate`, event emitters with separate callbacks), the context is lost. Use `AsyncResource.bind()` if you need to preserve context across such boundaries.
555
+
556
+ - **`init()` caches permanently for the event** — If you call `init('key', getterA)` and later `init('key', getterB)`, `getterB` is never called. The value from `getterA` is returned. This is by design — lazy initialization, not lazy re-computation.
557
+
558
+ - **Store sections start as `undefined`** — `store('mySection').value` is `undefined` until something writes to it. The first call to `init()`, `set()`, or assignment to `.value` creates the section.
559
+
560
+ - **Event type validation** — When calling `useAsyncEventContext('MY_EVENT')`, the system verifies the current context has `event.type === 'MY_EVENT'`. If it doesn't match, it checks the parent context. If neither matches, it throws a type mismatch error. This prevents accidentally using an HTTP composable inside a CLI event.
561
+
562
+ - **Don't hold references across events** — Values from the store are scoped to one event. Don't cache them in module-level variables or closures that outlive the event.