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