@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/README.md +34 -4
- package/dist/index.cjs +440 -221
- package/dist/index.d.ts +386 -107
- package/dist/index.mjs +422 -211
- package/package.json +11 -13
- package/scripts/setup-skills.js +78 -0
- package/skills/wooksjs-event-core/.gitkeep +0 -0
- package/skills/wooksjs-event-core/SKILL.md +50 -0
- package/skills/wooksjs-event-core/composables.md +200 -0
- package/skills/wooksjs-event-core/context.md +270 -0
- package/skills/wooksjs-event-core/core.md +91 -0
- package/skills/wooksjs-event-core/primitives.md +213 -0
package/dist/index.mjs
CHANGED
|
@@ -1,287 +1,498 @@
|
|
|
1
|
-
import { randomUUID } from "crypto";
|
|
2
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
-
import {
|
|
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
|
-
*
|
|
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
|
-
*
|
|
10
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
240
|
+
* Replaces the global `ContextInjector` with a custom implementation.
|
|
241
|
+
* Use this to integrate OpenTelemetry or other observability tools.
|
|
33
242
|
*
|
|
34
|
-
* @param
|
|
35
|
-
*
|
|
36
|
-
* @
|
|
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
|
|
39
|
-
|
|
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/
|
|
48
|
-
const STORAGE_KEY = Symbol.for("wooks.asyncStorage");
|
|
49
|
-
const VERSION_KEY = Symbol.for("wooks.asyncStorage.version");
|
|
50
|
-
const CURRENT_VERSION = "0.6.
|
|
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)
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
292
|
+
function run(ctx, fn) {
|
|
293
|
+
return storage.run(ctx, fn);
|
|
294
|
+
}
|
|
66
295
|
/**
|
|
67
|
-
*
|
|
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
|
-
*
|
|
70
|
-
*
|
|
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
|
|
73
|
-
|
|
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
|
-
*
|
|
317
|
+
* Returns the logger for the current event context.
|
|
318
|
+
*
|
|
319
|
+
* @param ctx - Optional explicit context (defaults to `current()`)
|
|
81
320
|
*
|
|
82
|
-
* @
|
|
83
|
-
*
|
|
321
|
+
* @example
|
|
322
|
+
* ```ts
|
|
323
|
+
* const logger = useLogger()
|
|
324
|
+
* logger.info('Processing request')
|
|
325
|
+
* ```
|
|
84
326
|
*/
|
|
85
|
-
function
|
|
86
|
-
|
|
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
|
|
97
|
-
const
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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/
|
|
340
|
+
//#region packages/event-core/src/cached-by.ts
|
|
210
341
|
/**
|
|
211
|
-
*
|
|
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
|
|
216
|
-
*
|
|
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
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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/
|
|
228
|
-
/**
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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/
|
|
423
|
+
//#region packages/event-core/src/wook.ts
|
|
243
424
|
/**
|
|
244
|
-
*
|
|
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
|
|
250
|
-
*
|
|
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
|
|
254
|
-
const
|
|
255
|
-
|
|
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
|
|
455
|
+
//#region packages/event-core/src/composables.ts
|
|
456
|
+
const eventIdSlot = cached(() => randomUUID());
|
|
264
457
|
/**
|
|
265
|
-
*
|
|
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
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
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
|
|
276
|
-
const params =
|
|
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,
|
|
498
|
+
export { ContextInjector, EventContext, cached, cachedBy, createEventContext, current, defineEventKind, defineWook, eventTypeKey, getContextInjector, key, replaceContextInjector, routeParamsKey, run, slot, tryGetCurrent, useEventId, useLogger, useRouteParams };
|