@wooksjs/event-http 0.6.2 → 0.6.3
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/README.md +24 -0
- package/dist/index.cjs +132 -25
- package/dist/index.d.ts +110 -12
- package/dist/index.mjs +132 -25
- package/package.json +45 -37
- package/scripts/setup-skills.js +70 -0
- package/skills/wooksjs-event-http/SKILL.md +37 -0
- package/skills/wooksjs-event-http/addons.md +307 -0
- package/skills/wooksjs-event-http/core.md +297 -0
- package/skills/wooksjs-event-http/error-handling.md +253 -0
- package/skills/wooksjs-event-http/event-core.md +562 -0
- package/skills/wooksjs-event-http/request.md +220 -0
- package/skills/wooksjs-event-http/response.md +336 -0
- package/skills/wooksjs-event-http/routing.md +412 -0
|
@@ -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.
|