@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.
package/dist/index.mjs CHANGED
@@ -1,287 +1,498 @@
1
- import { randomUUID } from "crypto";
2
1
  import { AsyncLocalStorage } from "node:async_hooks";
3
- import { ProstoLogger, coloredConsole, createConsoleTransort } from "@prostojs/logger";
2
+ import { randomUUID } from "node:crypto";
4
3
 
4
+ //#region packages/event-core/src/key.ts
5
+ let nextId = 0;
6
+ /**
7
+ * Creates a typed, writable context slot. Use `ctx.set(k, value)` to store
8
+ * and `ctx.get(k)` to retrieve. Throws if read before being set.
9
+ *
10
+ * @param name - Debug label (shown in error messages, not used for lookup)
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * const userIdKey = key<string>('userId')
15
+ * ctx.set(userIdKey, '123')
16
+ * ctx.get(userIdKey) // '123'
17
+ * ```
18
+ */
19
+ function key(name) {
20
+ return {
21
+ _id: nextId++,
22
+ _name: name
23
+ };
24
+ }
25
+ /**
26
+ * Creates a lazily-computed, read-only context slot. The factory runs once
27
+ * per `EventContext` on first `ctx.get(slot)` call; the result is cached
28
+ * for the context lifetime. Errors are also cached and re-thrown.
29
+ *
30
+ * @param fn - Factory receiving the current `EventContext`, returning the value to cache
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * const parsedUrl = cached((ctx) => new URL(ctx.get(rawUrlKey)))
35
+ * // first call computes, subsequent calls return cached result
36
+ * ctx.get(parsedUrl)
37
+ * ```
38
+ */
39
+ function cached(fn) {
40
+ return {
41
+ _id: nextId++,
42
+ _name: `cached:${nextId}`,
43
+ _fn: fn
44
+ };
45
+ }
46
+ /** @internal Returns true if the accessor is a `Cached` slot (has a factory function). */
47
+ function isCached(accessor) {
48
+ return "_fn" in accessor;
49
+ }
50
+
51
+ //#endregion
52
+ //#region packages/event-core/src/keys.ts
53
+ /** Context key for route parameters. Set by adapters after route matching. */
54
+ const routeParamsKey = key("routeParams");
55
+ /** Context key for the event type name (e.g. `'http'`, `'cli'`). Set by `ctx.seed()`. */
56
+ const eventTypeKey = key("eventType");
57
+
58
+ //#endregion
59
+ //#region packages/event-core/src/context.ts
60
+ const COMPUTING = Symbol("computing");
61
+ const UNDEFINED = Symbol("undefined");
62
+ var CachedError = class {
63
+ constructor(error) {
64
+ this.error = error;
65
+ }
66
+ };
67
+ /**
68
+ * Per-event container for typed slots, propagated via `AsyncLocalStorage`.
69
+ * Composables read and write data through `get`/`set` using typed `Key` or `Cached` accessors.
70
+ *
71
+ * Supports a parent chain: when a slot is not found locally, `get()` traverses
72
+ * parent contexts. `set()` writes to the nearest context that already holds the slot,
73
+ * or locally if the slot is new.
74
+ *
75
+ * Typically created by adapters (HTTP, CLI, etc.) — application code
76
+ * interacts with it indirectly through composables.
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * const ctx = new EventContext({ logger })
81
+ * ctx.set(userIdKey, '123')
82
+ * ctx.get(userIdKey) // '123'
83
+ * ```
84
+ */
85
+ var EventContext = class {
86
+ /** Logger instance for this event. */
87
+ logger;
88
+ /** Parent context for read-through and scoped writes. */
89
+ parent;
90
+ slots = /* @__PURE__ */ new Map();
91
+ constructor(options) {
92
+ this.logger = options.logger;
93
+ this.parent = options.parent;
94
+ }
95
+ /**
96
+ * Reads a value from a typed slot.
97
+ * - For `Key<T>`: returns the previously `set` value, checking parent chain if not found locally.
98
+ * - For `Cached<T>`: returns a cached result from this context or any parent. If not found
99
+ * anywhere, runs the factory on first access, caches locally, and returns the result.
100
+ * Throws on circular dependencies. Errors are cached and re-thrown on subsequent access.
101
+ */
102
+ get(accessor) {
103
+ const id = accessor._id;
104
+ let val = this.slots.get(id);
105
+ if (val === void 0 && this.parent) val = this.parent._findSlot(id);
106
+ if (val !== void 0) {
107
+ if (val === COMPUTING) throw new Error(`Circular dependency detected for "${accessor._name}"`);
108
+ if (val instanceof CachedError) throw val.error;
109
+ if (val === UNDEFINED) return void 0;
110
+ return val;
111
+ }
112
+ if (isCached(accessor)) {
113
+ this.slots.set(id, COMPUTING);
114
+ try {
115
+ const result = accessor._fn(this);
116
+ this.slots.set(id, result === void 0 ? UNDEFINED : result);
117
+ return result;
118
+ } catch (error) {
119
+ this.slots.set(id, new CachedError(error));
120
+ throw error;
121
+ }
122
+ }
123
+ throw new Error(`Key "${accessor._name}" is not set`);
124
+ }
125
+ /**
126
+ * Writes a value to a typed slot. If the slot already exists somewhere in the
127
+ * parent chain, the value is written there. Otherwise, it is written locally.
128
+ */
129
+ set(key$1, value) {
130
+ const encoded = value === void 0 ? UNDEFINED : value;
131
+ if (this.slots.has(key$1._id)) {
132
+ this.slots.set(key$1._id, encoded);
133
+ return;
134
+ }
135
+ if (this.parent && this.parent._setIfExists(key$1._id, encoded)) return;
136
+ this.slots.set(key$1._id, encoded);
137
+ }
138
+ /**
139
+ * Returns `true` if the slot has been set or computed in this context or any parent.
140
+ */
141
+ has(accessor) {
142
+ const val = this.slots.get(accessor._id);
143
+ if (val !== void 0 && val !== COMPUTING) return true;
144
+ return this.parent?.has(accessor) ?? false;
145
+ }
146
+ /**
147
+ * Reads a value from a typed slot in this context only, ignoring parents.
148
+ * Same semantics as `get()` but without parent chain traversal.
149
+ */
150
+ getOwn(accessor) {
151
+ const id = accessor._id;
152
+ const val = this.slots.get(id);
153
+ if (val !== void 0) {
154
+ if (val === COMPUTING) throw new Error(`Circular dependency detected for "${accessor._name}"`);
155
+ if (val instanceof CachedError) throw val.error;
156
+ if (val === UNDEFINED) return void 0;
157
+ return val;
158
+ }
159
+ if (isCached(accessor)) {
160
+ this.slots.set(id, COMPUTING);
161
+ try {
162
+ const result = accessor._fn(this);
163
+ this.slots.set(id, result === void 0 ? UNDEFINED : result);
164
+ return result;
165
+ } catch (error) {
166
+ this.slots.set(id, new CachedError(error));
167
+ throw error;
168
+ }
169
+ }
170
+ throw new Error(`Key "${accessor._name}" is not set`);
171
+ }
172
+ /**
173
+ * Writes a value to a typed slot in this context only, ignoring parents.
174
+ */
175
+ setOwn(key$1, value) {
176
+ this.slots.set(key$1._id, value === void 0 ? UNDEFINED : value);
177
+ }
178
+ /**
179
+ * Returns `true` if the slot has been set or computed in this context only.
180
+ */
181
+ hasOwn(accessor) {
182
+ const val = this.slots.get(accessor._id);
183
+ return val !== void 0 && val !== COMPUTING;
184
+ }
185
+ seed(kind, seeds, fn) {
186
+ const entries = kind._entries;
187
+ for (const [prop, k] of entries) {
188
+ const v = seeds[prop];
189
+ this.slots.set(k._id, v === void 0 ? UNDEFINED : v);
190
+ }
191
+ this.setOwn(eventTypeKey, kind.name);
192
+ if (fn) return fn();
193
+ }
194
+ /** Walk the parent chain looking for a set slot. Returns `undefined` if not found. */
195
+ _findSlot(id) {
196
+ const val = this.slots.get(id);
197
+ if (val !== void 0) return val;
198
+ return this.parent?._findSlot(id);
199
+ }
200
+ /** Set value in the first context in the chain that has this slot. Returns true if found. */
201
+ _setIfExists(id, encoded) {
202
+ if (this.slots.has(id)) {
203
+ this.slots.set(id, encoded);
204
+ return true;
205
+ }
206
+ return this.parent?._setIfExists(id, encoded) ?? false;
207
+ }
208
+ };
209
+
210
+ //#endregion
5
211
  //#region packages/event-core/src/context-injector.ts
6
212
  /**
7
- * ContextInjector
213
+ * No-op base class for observability integration. Subclass and override
214
+ * `with()` / `hook()` to add tracing, metrics, or logging around event
215
+ * lifecycle points.
8
216
  *
9
- * Provides a way to inject context
10
- * Usefull when working with opentelemetry spans
217
+ * The default implementation simply calls the callback with no overhead.
218
+ * Replace via `replaceContextInjector()` to enable instrumentation.
11
219
  */
12
220
  var ContextInjector = class {
13
221
  with(name, attributes, cb) {
14
222
  const fn = typeof attributes === "function" ? attributes : cb;
15
223
  return fn();
16
224
  }
225
+ /**
226
+ * Hook called by adapters at specific lifecycle points (e.g., after route lookup).
227
+ * Default implementation is a no-op — override for observability.
228
+ */
17
229
  hook(_method, _name, _route) {}
18
230
  };
19
231
  let ci = new ContextInjector();
20
- /** Returns the current global `ContextInjector` instance. */
232
+ /**
233
+ * Returns the current `ContextInjector` instance (default: no-op).
234
+ * Used internally by adapters to wrap lifecycle events.
235
+ */
21
236
  function getContextInjector() {
22
237
  return ci;
23
238
  }
24
- /** Replaces the global `ContextInjector` instance (e.g., to integrate with OpenTelemetry). */
25
- function replaceContextInjector(newCi) {
26
- ci = newCi;
27
- }
28
-
29
- //#endregion
30
- //#region packages/event-core/src/hook.ts
31
239
  /**
32
- * Attaches a getter/setter hook to a target object property via `Object.defineProperty`.
240
+ * Replaces the global `ContextInjector` with a custom implementation.
241
+ * Use this to integrate OpenTelemetry or other observability tools.
33
242
  *
34
- * @param target - The object to attach the hook to.
35
- * @param opts - Getter and optional setter for the hooked property.
36
- * @param name - Property name to hook (defaults to `'value'`).
243
+ * @param newCi - Custom `ContextInjector` subclass instance
244
+ *
245
+ * @example
246
+ * ```ts
247
+ * class OtelInjector extends ContextInjector<string> {
248
+ * with<T>(name: string, attrs: Record<string, any>, cb: () => T): T {
249
+ * return tracer.startActiveSpan(name, (span) => {
250
+ * span.setAttributes(attrs)
251
+ * try { return cb() } finally { span.end() }
252
+ * })
253
+ * }
254
+ * }
255
+ * replaceContextInjector(new OtelInjector())
256
+ * ```
37
257
  */
38
- function attachHook(target, opts, name) {
39
- Object.defineProperty(target, name || "value", {
40
- get: opts.get,
41
- set: opts.set
42
- });
43
- return target;
258
+ function replaceContextInjector(newCi) {
259
+ ci = newCi;
44
260
  }
45
261
 
46
262
  //#endregion
47
- //#region packages/event-core/src/context.ts
48
- const STORAGE_KEY = Symbol.for("wooks.asyncStorage");
49
- const VERSION_KEY = Symbol.for("wooks.asyncStorage.version");
50
- const CURRENT_VERSION = "0.6.2";
263
+ //#region packages/event-core/src/storage.ts
264
+ const STORAGE_KEY = Symbol.for("wooks.core.asyncStorage");
265
+ const VERSION_KEY = Symbol.for("wooks.core.asyncStorage.version");
266
+ const CURRENT_VERSION = "0.6.6";
51
267
  const _g = globalThis;
52
268
  if (_g[STORAGE_KEY]) {
53
- if (_g[VERSION_KEY] !== CURRENT_VERSION) console.warn(`[wooks] Multiple versions of @wooksjs/event-core detected: existing v${_g[VERSION_KEY]}, current v${CURRENT_VERSION}. Reusing existing asyncStorage to avoid runtime errors.`);
269
+ 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.`);
54
270
  } else {
55
271
  _g[STORAGE_KEY] = new AsyncLocalStorage();
56
272
  _g[VERSION_KEY] = CURRENT_VERSION;
57
273
  }
274
+ const storage = _g[STORAGE_KEY];
58
275
  /**
59
- * AsyncLocalStorage instance
276
+ * Runs a callback with the given `EventContext` as the active context.
277
+ * All composables and `current()` calls inside `fn` will resolve to `ctx`.
60
278
  *
61
- * Use on your own risk only if you know what you're doing
279
+ * @param ctx - The event context to make active
280
+ * @param fn - Callback to execute within the context scope
281
+ * @returns The return value of `fn`
282
+ *
283
+ * @example
284
+ * ```ts
285
+ * const ctx = new EventContext({ logger })
286
+ * run(ctx, () => {
287
+ * // current() returns ctx here
288
+ * const logger = useLogger()
289
+ * })
290
+ * ```
62
291
  */
63
- const asyncStorage = _g[STORAGE_KEY];
64
- /** Symbol key for caching store accessors directly on the context object */
65
- const _storeCacheKey = Symbol("storeCache");
292
+ function run(ctx, fn) {
293
+ return storage.run(ctx, fn);
294
+ }
66
295
  /**
67
- * Creates a new async event context and returns a runner function to execute callbacks within it.
296
+ * Returns the active `EventContext` for the current async scope.
297
+ * Throws if called outside an event context (e.g., at module level).
68
298
  *
69
- * @param data - Initial context store data including the event object and options.
70
- * @returns A function that runs a callback within the created async context.
299
+ * All composables use this internally. Prefer composables over direct `current()` access.
300
+ *
301
+ * @throws Error if no active event context exists
302
+ */
303
+ function current() {
304
+ const ctx = storage.getStore();
305
+ if (!ctx) throw new Error("[Wooks] No active event context");
306
+ return ctx;
307
+ }
308
+ /**
309
+ * Returns the active `EventContext`, or `undefined` if none is active.
310
+ * Use this when context availability is uncertain (e.g., in code that may
311
+ * run both inside and outside an event handler).
71
312
  */
72
- function createAsyncEventContext(data) {
73
- const newContext = { ...data };
74
- const cc = asyncStorage.getStore();
75
- if (cc && typeof cc === "object" && cc.event?.type) newContext.parentCtx = cc;
76
- const ci$1 = getContextInjector();
77
- return (cb) => asyncStorage.run(newContext, () => ci$1.with("Event:start", { eventType: newContext.event.type }, cb));
313
+ function tryGetCurrent() {
314
+ return storage.getStore();
78
315
  }
79
316
  /**
80
- * Retrieves the current async event context and returns helpers for reading/writing the store.
317
+ * Returns the logger for the current event context.
318
+ *
319
+ * @param ctx - Optional explicit context (defaults to `current()`)
81
320
  *
82
- * @param expectedTypes - Optional event type(s) to validate the context against.
83
- * @throws If no event context exists or if the event type does not match.
321
+ * @example
322
+ * ```ts
323
+ * const logger = useLogger()
324
+ * logger.info('Processing request')
325
+ * ```
84
326
  */
85
- function useAsyncEventContext(expectedTypes) {
86
- let cc = asyncStorage.getStore();
87
- if (!cc) throw new Error("Event context does not exist at this point.");
88
- if (expectedTypes || typeof expectedTypes === "string") {
89
- const type = cc.event.type;
90
- const types = typeof expectedTypes === "string" ? [expectedTypes] : expectedTypes;
91
- if (!types.includes(type)) if (cc.parentCtx?.event.type && types.includes(cc.parentCtx.event.type)) cc = cc.parentCtx;
92
- else throw new Error(`Event context type mismatch: expected ${types.map((t) => `"${t}"`).join(", ")}, received "${type}"`);
93
- }
94
- return _getCtxHelpers(cc);
327
+ function useLogger(ctx) {
328
+ return (ctx ?? current()).logger;
95
329
  }
96
- function _getCtxHelpers(cc) {
97
- const ccAny = cc;
98
- if (!ccAny[_storeCacheKey]) ccAny[_storeCacheKey] = /* @__PURE__ */ new Map();
99
- const _storeCache = ccAny[_storeCacheKey];
100
- /**
101
- * Hook to an event store property
102
- *
103
- * @param key store property key
104
- * @returns a hook { value: <prop value>, hook: (key2: keyof <prop value>) => { value: <nested prop value> }, ... }
105
- */
106
- function store(key) {
107
- const cachedStore = _storeCache.get(key);
108
- if (cachedStore) return cachedStore;
109
- const getSection = () => cc[key];
110
- const setSection = (v) => {
111
- cc[key] = v;
112
- };
113
- const obj = {
114
- value: null,
115
- hook,
116
- init,
117
- set: setNested,
118
- get: getNested,
119
- has: hasNested,
120
- del: delNested,
121
- entries,
122
- clear
123
- };
124
- attachHook(obj, {
125
- set: (v) => {
126
- setSection(v);
127
- },
128
- get: () => getSection()
129
- });
130
- function init(key2, getter) {
131
- if (hasNested(key2)) return getNested(key2);
132
- return setNested(key2, getter());
133
- }
134
- function hook(key2) {
135
- const hookObj = {
136
- value: null,
137
- isDefined: null
138
- };
139
- attachHook(hookObj, {
140
- set: (v) => setNested(key2, v),
141
- get: () => getNested(key2)
142
- });
143
- attachHook(hookObj, { get: () => hasNested(key2) }, "isDefined");
144
- return hookObj;
145
- }
146
- function setNested(key2, v) {
147
- let section = getSection();
148
- if (section === void 0) {
149
- section = {};
150
- setSection(section);
151
- }
152
- section[key2] = v;
153
- return v;
154
- }
155
- function delNested(key2) {
156
- setNested(key2, void 0);
157
- }
158
- function getNested(key2) {
159
- const section = getSection();
160
- return section !== void 0 ? section[key2] : void 0;
161
- }
162
- function hasNested(key2) {
163
- const section = getSection();
164
- return section !== void 0 ? section[key2] !== void 0 : false;
165
- }
166
- function entries() {
167
- const section = getSection();
168
- return section ? Object.entries(section) : [];
169
- }
170
- function clear() {
171
- setSection({});
172
- }
173
- _storeCache.set(key, obj);
174
- return obj;
175
- }
176
- /**
177
- * Get event context object
178
- *
179
- * @returns whole context object
180
- */
181
- function getCtx() {
182
- return cc;
183
- }
184
- function get(key) {
185
- return cc[key];
186
- }
187
- function set(key, v) {
188
- cc[key] = v;
189
- }
190
- const hasParentCtx = () => !!cc.parentCtx;
191
- const helpers = {
192
- getCtx,
193
- store,
194
- getStore: get,
195
- setStore: set,
196
- setParentCtx: (parentCtx) => {
197
- cc.parentCtx = parentCtx;
198
- },
199
- hasParentCtx,
200
- getParentCtx: () => {
201
- if (!hasParentCtx()) throw new Error("Parent context is not available");
202
- return _getCtxHelpers(cc.parentCtx);
203
- }
204
- };
205
- return helpers;
330
+ function createEventContext(options, kindOrFn, seedsOrUndefined, maybeFn) {
331
+ const ctx = new EventContext(options);
332
+ if (typeof kindOrFn === "function") return run(ctx, kindOrFn);
333
+ return run(ctx, () => {
334
+ ctx.seed(kindOrFn, seedsOrUndefined);
335
+ return getContextInjector().with("Event:start", { eventType: kindOrFn.name }, maybeFn);
336
+ });
206
337
  }
207
338
 
208
339
  //#endregion
209
- //#region packages/event-core/src/composables/event-id.ts
340
+ //#region packages/event-core/src/cached-by.ts
210
341
  /**
211
- * Composable that provides a unique event ID for the current event context.
342
+ * Creates a parameterized cached computation. Maintains a `Map<K, V>` per
343
+ * event context — one cached result per unique key argument.
344
+ *
345
+ * @param fn - Factory receiving the lookup key and `EventContext`, returning the value to cache
346
+ * @returns A function `(key: K, ctx?: EventContext) => V` that computes on first call per key
212
347
  *
213
348
  * @example
214
349
  * ```ts
215
- * const { getId } = useEventId()
216
- * console.log(getId()) // '550e8400-e29b-41d4-a716-446655440000'
350
+ * const parseCookie = cachedBy((name: string, ctx) => {
351
+ * const raw = ctx.get(cookieHeaderKey)
352
+ * return parseSingleCookie(raw, name)
353
+ * })
354
+ *
355
+ * parseCookie('session') // computed and cached for 'session'
356
+ * parseCookie('theme') // computed and cached for 'theme'
357
+ * parseCookie('session') // returns cached result
217
358
  * ```
218
359
  */
219
- function useEventId() {
220
- const { store } = useAsyncEventContext();
221
- const { init } = store("event");
222
- const getId = () => init("id", () => randomUUID());
223
- return { getId };
360
+ function cachedBy(fn) {
361
+ const mapSlot = cached(() => /* @__PURE__ */ new Map());
362
+ return (k, ctx) => {
363
+ const c = ctx ?? current();
364
+ const map = c.get(mapSlot);
365
+ if (!map.has(k)) map.set(k, fn(k, c));
366
+ return map.get(k);
367
+ };
224
368
  }
225
369
 
226
370
  //#endregion
227
- //#region packages/event-core/src/event-logger.ts
228
- /** Logger scoped to a single event, automatically tagging messages with the event ID. */
229
- var EventLogger = class extends ProstoLogger {
230
- constructor(eventId, opts) {
231
- const _opts = opts || { level: 4 };
232
- if (!_opts.mapper) _opts.mapper = (msg) => ({
233
- ...msg,
234
- eventId
235
- });
236
- if (!_opts.transports) _opts.transports = [createConsoleTransort({ format: coloredConsole })];
237
- super(_opts, opts?.topic || "event");
371
+ //#region packages/event-core/src/kind.ts
372
+ /**
373
+ * Type-level marker used inside `defineEventKind` schemas. Each `slot<T>()`
374
+ * becomes a typed `Key<T>` on the resulting `EventKind`. Has no runtime behavior.
375
+ *
376
+ * @example
377
+ * ```ts
378
+ * const httpKind = defineEventKind('http', {
379
+ * req: slot<IncomingMessage>(),
380
+ * response: slot<HttpResponse>(),
381
+ * })
382
+ * ```
383
+ */
384
+ function slot() {
385
+ return {};
386
+ }
387
+ /**
388
+ * Declares a named event kind with typed seed slots. The returned object
389
+ * contains `keys` — typed accessors for reading seed values from context —
390
+ * and is passed to `ctx.seed(kind, seeds)` or `createEventContext()`.
391
+ *
392
+ * @param name - Unique event kind name (e.g. `'http'`, `'cli'`, `'workflow'`)
393
+ * @param schema - Object mapping slot names to `slot<T>()` markers
394
+ * @returns An `EventKind` with typed `keys` for context access
395
+ *
396
+ * @example
397
+ * ```ts
398
+ * const httpKind = defineEventKind('http', {
399
+ * req: slot<IncomingMessage>(),
400
+ * response: slot<HttpResponse>(),
401
+ * })
402
+ *
403
+ * // Access typed seed values:
404
+ * const req = ctx.get(httpKind.keys.req) // IncomingMessage
405
+ * ```
406
+ */
407
+ function defineEventKind(name, schema) {
408
+ const keys = {};
409
+ const _entries = [];
410
+ for (const prop of Object.keys(schema)) {
411
+ const k = key(`${name}.${prop}`);
412
+ keys[prop] = k;
413
+ _entries.push([prop, k]);
238
414
  }
239
- };
415
+ return {
416
+ name,
417
+ keys,
418
+ _entries
419
+ };
420
+ }
240
421
 
241
422
  //#endregion
242
- //#region packages/event-core/src/composables/event-logger.ts
423
+ //#region packages/event-core/src/wook.ts
243
424
  /**
244
- * Composable that provides a logger scoped to the current event context.
425
+ * Creates a composable with per-event caching. The factory runs once per
426
+ * `EventContext`; subsequent calls within the same event return the cached result.
427
+ *
428
+ * This is the recommended way to build composables in Wooks. All built-in
429
+ * composables (`useRequest`, `useResponse`, `useCookies`, etc.) are created with `defineWook`.
430
+ *
431
+ * @param factory - Receives the `EventContext` and returns the composable's public API
432
+ * @returns A composable function `(ctx?: EventContext) => T`
245
433
  *
246
- * @param topic - Optional topic name to create a sub-logger for.
247
434
  * @example
248
435
  * ```ts
249
- * const logger = useEventLogger('my-handler')
250
- * logger.log('processing request')
436
+ * export const useCurrentUser = defineWook((ctx) => {
437
+ * const { basicCredentials } = useAuthorization(ctx)
438
+ * const username = basicCredentials()?.username
439
+ * return {
440
+ * username,
441
+ * profile: async () => username ? await db.findUser(username) : null,
442
+ * }
443
+ * })
444
+ *
445
+ * // In a handler — factory runs once, cached for the request:
446
+ * const { username, profile } = useCurrentUser()
251
447
  * ```
252
448
  */
253
- function useEventLogger(topic) {
254
- const { getId } = useEventId();
255
- const { store, getCtx } = useAsyncEventContext();
256
- const { init } = store("event");
257
- const ctx = getCtx();
258
- const get = () => init("logger", () => new EventLogger(getId(), ctx.options.eventLogger));
259
- return topic ? get().createTopic(topic) : get();
449
+ function defineWook(factory) {
450
+ const slot$1 = cached(factory);
451
+ return (ctx) => (ctx ?? current()).get(slot$1);
260
452
  }
261
453
 
262
454
  //#endregion
263
- //#region packages/event-core/src/composables/route-params.ts
455
+ //#region packages/event-core/src/composables.ts
456
+ const eventIdSlot = cached(() => randomUUID());
264
457
  /**
265
- * Composable that provides access to route parameters from the current event context.
458
+ * Returns the route parameters for the current event. Works with HTTP
459
+ * routes, CLI commands, workflow steps — any adapter that sets `routeParamsKey`.
460
+ *
461
+ * @param ctx - Optional explicit context (defaults to `current()`)
462
+ * @returns Object with `params` (the full params record) and `get(name)` for typed access
266
463
  *
267
464
  * @example
268
465
  * ```ts
269
- * const { get, params } = useRouteParams<{ id: string }>()
270
- * console.log(get('id')) // '123'
271
- * console.log(params) // { id: '123' }
466
+ * app.get('/users/:id', () => {
467
+ * const { params, get } = useRouteParams<{ id: string }>()
468
+ * console.log(get('id')) // typed as string
469
+ * })
272
470
  * ```
273
471
  */
274
- function useRouteParams() {
275
- const { store } = useAsyncEventContext();
276
- const params = store("routeParams").value || {};
277
- function get(name) {
278
- return params[name];
279
- }
472
+ function useRouteParams(ctx) {
473
+ const c = ctx ?? current();
474
+ const params = c.get(routeParamsKey);
280
475
  return {
281
476
  params,
282
- get
477
+ get: (name) => params[name]
283
478
  };
284
479
  }
480
+ /**
481
+ * Provides a unique, per-event identifier. The ID is a random UUID, generated
482
+ * lazily on first `getId()` call and cached for the event lifetime.
483
+ *
484
+ * @param ctx - Optional explicit context (defaults to `current()`)
485
+ *
486
+ * @example
487
+ * ```ts
488
+ * const { getId } = useEventId()
489
+ * logger.info(`Request ${getId()}`)
490
+ * ```
491
+ */
492
+ function useEventId(ctx) {
493
+ const c = ctx ?? current();
494
+ return { getId: () => c.get(eventIdSlot) };
495
+ }
285
496
 
286
497
  //#endregion
287
- export { ContextInjector, EventLogger, asyncStorage, attachHook, createAsyncEventContext, getContextInjector, replaceContextInjector, useAsyncEventContext, useEventId, useEventLogger, useRouteParams };
498
+ export { ContextInjector, EventContext, cached, cachedBy, createEventContext, current, defineEventKind, defineWook, eventTypeKey, getContextInjector, key, replaceContextInjector, routeParamsKey, run, slot, tryGetCurrent, useEventId, useLogger, useRouteParams };