@wooksjs/event-core 0.6.6 → 0.7.1

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,309 +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.7.0";
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");
89
- /** Symbol key for caching the full helpers object on the context */
90
- const _helpersCacheKey = Symbol("helpersCache");
315
+ function run(ctx, fn) {
316
+ return storage.run(ctx, fn);
317
+ }
91
318
  /**
92
- * 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.
93
323
  *
94
- * @param data - Initial context store data including the event object and options.
95
- * @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).
96
335
  */
97
- function createAsyncEventContext(data) {
98
- const newContext = { ...data };
99
- const cc = asyncStorage.getStore();
100
- if (cc && typeof cc === "object" && cc.event?.type) newContext.parentCtx = cc;
101
- const ci$1 = getContextInjector();
102
- return (cb) => asyncStorage.run(newContext, () => ci$1.with("Event:start", { eventType: newContext.event.type }, cb));
336
+ function tryGetCurrent() {
337
+ return storage.getStore();
103
338
  }
104
339
  /**
105
- * 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()`)
106
343
  *
107
- * @param expectedTypes - Optional event type(s) to validate the context against.
108
- * @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
+ * ```
109
349
  */
110
- function useAsyncEventContext(expectedTypes) {
111
- let cc = asyncStorage.getStore();
112
- if (!cc) throw new Error("Event context does not exist at this point.");
113
- if (expectedTypes || typeof expectedTypes === "string") {
114
- const type = cc.event.type;
115
- const types = typeof expectedTypes === "string" ? [expectedTypes] : expectedTypes;
116
- if (!types.includes(type)) if (cc.parentCtx?.event.type && types.includes(cc.parentCtx.event.type)) cc = cc.parentCtx;
117
- else throw new Error(`Event context type mismatch: expected ${types.map((t) => `"${t}"`).join(", ")}, received "${type}"`);
118
- }
119
- return _getCtxHelpers(cc);
350
+ function useLogger(ctx) {
351
+ return (ctx ?? current()).logger;
120
352
  }
121
- function _getCtxHelpers(cc) {
122
- const ccRec = cc;
123
- if (ccRec[_helpersCacheKey]) return ccRec[_helpersCacheKey];
124
- if (!ccRec[_storeCacheKey]) ccRec[_storeCacheKey] = /* @__PURE__ */ new Map();
125
- const _storeCache = ccRec[_storeCacheKey];
126
- /**
127
- * Hook to an event store property
128
- *
129
- * @param key store property key
130
- * @returns a hook { value: <prop value>, hook: (key2: keyof <prop value>) => { value: <nested prop value> }, ... }
131
- */
132
- function store(key) {
133
- const cachedStore = _storeCache.get(key);
134
- if (cachedStore) return cachedStore;
135
- const getSection = () => cc[key];
136
- const setSection = (v) => {
137
- cc[key] = v;
138
- };
139
- const _hookCache = /* @__PURE__ */ new Map();
140
- const obj = {
141
- value: null,
142
- hook,
143
- init,
144
- set: setNested,
145
- get: getNested,
146
- has: hasNested,
147
- del: delNested,
148
- entries,
149
- clear
150
- };
151
- attachHook(obj, {
152
- set: (v) => {
153
- setSection(v);
154
- },
155
- get: () => getSection()
156
- });
157
- function init(key2, getter) {
158
- if (hasNested(key2)) return getNested(key2);
159
- return setNested(key2, getter());
160
- }
161
- function hook(key2) {
162
- const hookKey = key2;
163
- const cached = _hookCache.get(hookKey);
164
- if (cached) return cached;
165
- const hookObj = {
166
- value: null,
167
- isDefined: null
168
- };
169
- attachHook(hookObj, {
170
- set: (v) => setNested(key2, v),
171
- get: () => getNested(key2)
172
- });
173
- attachHook(hookObj, { get: () => hasNested(key2) }, "isDefined");
174
- _hookCache.set(hookKey, hookObj);
175
- return hookObj;
176
- }
177
- function setNested(key2, v) {
178
- let section = getSection();
179
- if (section === void 0) {
180
- section = {};
181
- setSection(section);
182
- }
183
- section[key2] = v;
184
- return v;
185
- }
186
- function delNested(key2) {
187
- setNested(key2, void 0);
188
- }
189
- function getNested(key2) {
190
- const section = getSection();
191
- return section !== void 0 ? section[key2] : void 0;
192
- }
193
- function hasNested(key2) {
194
- const section = getSection();
195
- return section !== void 0 ? section[key2] !== void 0 : false;
196
- }
197
- function entries() {
198
- const section = getSection();
199
- return section ? Object.entries(section) : [];
200
- }
201
- function clear() {
202
- setSection({});
203
- }
204
- _storeCache.set(key, obj);
205
- return obj;
206
- }
207
- /**
208
- * Get event context object
209
- *
210
- * @returns whole context object
211
- */
212
- function getCtx() {
213
- return cc;
214
- }
215
- function get(key) {
216
- return cc[key];
217
- }
218
- function set(key, v) {
219
- cc[key] = v;
220
- }
221
- const hasParentCtx = () => !!cc.parentCtx;
222
- const helpers = {
223
- getCtx,
224
- store,
225
- getStore: get,
226
- setStore: set,
227
- setParentCtx: (parentCtx) => {
228
- cc.parentCtx = parentCtx;
229
- },
230
- hasParentCtx,
231
- getParentCtx: () => {
232
- if (!hasParentCtx()) throw new Error("Parent context is not available");
233
- return _getCtxHelpers(cc.parentCtx);
234
- }
235
- };
236
- ccRec[_helpersCacheKey] = helpers;
237
- 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
+ });
238
360
  }
239
361
 
240
362
  //#endregion
241
- //#region packages/event-core/src/composables/event-id.ts
363
+ //#region packages/event-core/src/cached-by.ts
242
364
  /**
243
- * 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
244
370
  *
245
371
  * @example
246
372
  * ```ts
247
- * const { getId } = useEventId()
248
- * 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
249
381
  * ```
250
382
  */
251
- function useEventId() {
252
- const { store } = useAsyncEventContext();
253
- const { init } = store("event");
254
- const getId = () => init("id", () => (0, crypto.randomUUID)());
255
- 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
+ };
256
391
  }
257
392
 
258
393
  //#endregion
259
- //#region packages/event-core/src/event-logger.ts
260
- /** Logger scoped to a single event, automatically tagging messages with the event ID. */
261
- var EventLogger = class extends __prostojs_logger.ProstoLogger {
262
- constructor(eventId, opts) {
263
- const _opts = opts || { level: 4 };
264
- if (!_opts.mapper) _opts.mapper = (msg) => ({
265
- ...msg,
266
- eventId
267
- });
268
- if (!_opts.transports) _opts.transports = [(0, __prostojs_logger.createConsoleTransort)({ format: __prostojs_logger.coloredConsole })];
269
- 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]);
270
437
  }
271
- };
438
+ return {
439
+ name,
440
+ keys,
441
+ _entries
442
+ };
443
+ }
272
444
 
273
445
  //#endregion
274
- //#region packages/event-core/src/composables/event-logger.ts
446
+ //#region packages/event-core/src/wook.ts
275
447
  /**
276
- * 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`
277
456
  *
278
- * @param topic - Optional topic name to create a sub-logger for.
279
457
  * @example
280
458
  * ```ts
281
- * const logger = useEventLogger('my-handler')
282
- * 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()
283
470
  * ```
284
471
  */
285
- function useEventLogger(topic) {
286
- const { getId } = useEventId();
287
- const { store, getCtx } = useAsyncEventContext();
288
- const { init } = store("event");
289
- const ctx = getCtx();
290
- const get = () => init("logger", () => new EventLogger(getId(), ctx.options.eventLogger));
291
- return topic ? get().createTopic(topic) : get();
472
+ function defineWook(factory) {
473
+ const slot$1 = cached(factory);
474
+ return (ctx) => (ctx ?? current()).get(slot$1);
292
475
  }
293
476
 
294
477
  //#endregion
295
- //#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)());
296
480
  /**
297
- * 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
298
486
  *
299
487
  * @example
300
488
  * ```ts
301
- * const { get, params } = useRouteParams<{ id: string }>()
302
- * console.log(get('id')) // '123'
303
- * 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
+ * })
304
493
  * ```
305
494
  */
306
- function useRouteParams() {
307
- const { store } = useAsyncEventContext();
308
- const params = store("routeParams").value || {};
309
- function get(name) {
310
- return params[name];
311
- }
495
+ function useRouteParams(ctx) {
496
+ const c = ctx ?? current();
497
+ const params = c.get(routeParamsKey);
312
498
  return {
313
499
  params,
314
- get
500
+ get: (name) => params[name]
315
501
  };
316
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
+ }
317
519
 
318
520
  //#endregion
319
521
  exports.ContextInjector = ContextInjector;
320
- exports.EventLogger = EventLogger;
321
- exports.asyncStorage = asyncStorage;
322
- exports.attachHook = attachHook;
323
- 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;
324
530
  exports.getContextInjector = getContextInjector;
531
+ exports.key = key;
325
532
  exports.replaceContextInjector = replaceContextInjector;
326
- exports.useAsyncEventContext = useAsyncEventContext;
533
+ exports.routeParamsKey = routeParamsKey;
534
+ exports.run = run;
535
+ exports.slot = slot;
536
+ exports.tryGetCurrent = tryGetCurrent;
327
537
  exports.useEventId = useEventId;
328
- exports.useEventLogger = useEventLogger;
538
+ exports.useLogger = useLogger;
329
539
  exports.useRouteParams = useRouteParams;