@stacksjs/events 0.72.98 → 0.72.100
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/discover.d.ts +58 -7
- package/dist/index.d.ts +95 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/dist/discover.d.ts
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
|
+
import type { EventName, StacksEvents } from './index';
|
|
2
|
+
/**
|
|
3
|
+
* Define a listener module, with the event names checked and the payload
|
|
4
|
+
* inferred from them.
|
|
5
|
+
*
|
|
6
|
+
* The same helper `defineEvents` is for `app/Events.ts`, for the other half of
|
|
7
|
+
* the convention: a plain object literal cannot infer `T` from `listensTo`, so
|
|
8
|
+
* without this the handler's payload widens to the union of everything the bus
|
|
9
|
+
* carries and the event names go unchecked.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* // app/Listeners/SendWelcomeEmail.ts
|
|
14
|
+
* import { defineListener } from '@stacksjs/events'
|
|
15
|
+
*
|
|
16
|
+
* export default defineListener({
|
|
17
|
+
* listensTo: 'user:registered',
|
|
18
|
+
* handle: async (user) => {
|
|
19
|
+
* // `user` is the registration payload, not `unknown`
|
|
20
|
+
* await mail.send({ to: user.email })
|
|
21
|
+
* },
|
|
22
|
+
* })
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function defineListener<const T extends EventSubscription>(listener: ListenerModule<T>): ListenerModule<T>;
|
|
1
26
|
/**
|
|
2
27
|
* Walk the listeners directory and register every default-exported
|
|
3
28
|
* listener that matches the `ListenerModule` shape. Returns the
|
|
@@ -33,15 +58,22 @@ export declare function resetListenerRegistry(): void;
|
|
|
33
58
|
* alternative is an application that will not boot over a typo in a notifier.
|
|
34
59
|
*/
|
|
35
60
|
export declare function registerAppListeners(options?: RegisterOptions): Promise<number>;
|
|
61
|
+
/** For tests, and for a dev server that regenerated the registries. */
|
|
62
|
+
export declare function resetNameRegistries(): void;
|
|
36
63
|
/**
|
|
37
|
-
* Shape of a listener module's default export.
|
|
38
|
-
*
|
|
39
|
-
* `
|
|
40
|
-
*
|
|
64
|
+
* Shape of a listener module's default export.
|
|
65
|
+
*
|
|
66
|
+
* `listensTo` was `string | string[]`, with a comment explaining that the loose
|
|
67
|
+
* type bought "cross-pkg flexibility". What it actually bought was a listener
|
|
68
|
+
* file that compiles while subscribing to an event that does not exist: the
|
|
69
|
+
* scan registers it, the emitter never matches it, and the file looks like it
|
|
70
|
+
* is doing its job forever. The names are checked now, and the payload follows
|
|
71
|
+
* from the name rather than being `unknown` for the handler to assert its way
|
|
72
|
+
* out of.
|
|
41
73
|
*/
|
|
42
|
-
export declare interface ListenerModule<T =
|
|
43
|
-
listensTo:
|
|
44
|
-
handle: (_payload: T
|
|
74
|
+
export declare interface ListenerModule<T extends EventSubscription = EventSubscription> {
|
|
75
|
+
listensTo: T | readonly T[]
|
|
76
|
+
handle: (_payload: SubscriptionPayload<T>, _eventName?: EventName) => void | Promise<void>
|
|
45
77
|
name?: string
|
|
46
78
|
}
|
|
47
79
|
declare interface DiscoverOptions {
|
|
@@ -56,5 +88,24 @@ declare interface DiscoverOptions {
|
|
|
56
88
|
declare interface RegisterOptions extends DiscoverOptions {
|
|
57
89
|
base?: string
|
|
58
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* What a listener may subscribe to: an event that exists, or a glob the
|
|
93
|
+
* emitter matches against every event name (`'user:*'`, `'*'`).
|
|
94
|
+
*
|
|
95
|
+
* The glob branch is why this is not simply `EventName`. `registerFromMap` and
|
|
96
|
+
* the scan both hand their names to `emitter.on`, which matches patterns as
|
|
97
|
+
* well as exact names, so a union without them would reject a subscription
|
|
98
|
+
* that works.
|
|
99
|
+
*/
|
|
100
|
+
export type EventSubscription = EventName | '*' | `${string}:*`;
|
|
101
|
+
/**
|
|
102
|
+
* The payload a subscription carries.
|
|
103
|
+
*
|
|
104
|
+
* Exact for an exact event name. A glob can fire for any event, so it gets the
|
|
105
|
+
* union of what the bus can carry rather than a guess at which one.
|
|
106
|
+
*/
|
|
107
|
+
export type SubscriptionPayload<T extends EventSubscription> = T extends EventName
|
|
108
|
+
? StacksEvents[T]
|
|
109
|
+
: StacksEvents[EventName];
|
|
59
110
|
/** An action's `validations:`, as much of it as this module needs. */
|
|
60
111
|
declare type PayloadValidations = Record<string, { rule?: { validate?: (_value: unknown) => { valid: boolean } } }>;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { ListenerModule } from './discover';
|
|
1
|
+
export type { EventSubscription, ListenerModule, SubscriptionPayload } from './discover';
|
|
2
2
|
/**
|
|
3
3
|
* Create a fresh Stacks event emitter. Most consumers want the singleton
|
|
4
4
|
* exported below — call this directly only when you need an isolated bus
|
|
@@ -42,6 +42,43 @@ export declare function scope<Events extends EventMap>(underlying: Emitter<Event
|
|
|
42
42
|
emitAndCollect: (type: string, event: unknown) => Promise<Array<{ ok: true, value: unknown } | { ok: false, error: Error }>>
|
|
43
43
|
listenerCount: (type: string) => number
|
|
44
44
|
};
|
|
45
|
+
/**
|
|
46
|
+
* Define the application's event-to-listener map, with the literal names kept.
|
|
47
|
+
*
|
|
48
|
+
* Identical in effect to `satisfies Events`, and preferred for the same reason
|
|
49
|
+
* every other `define*` helper in the framework is: the constraint is applied
|
|
50
|
+
* where the object is written, so the error points at the misspelled event name
|
|
51
|
+
* rather than at whatever consumed the map later.
|
|
52
|
+
*
|
|
53
|
+
* `const` on the type parameter is what makes it *narrower* than `satisfies`:
|
|
54
|
+
* the returned type keeps `readonly ['SendWelcomeEmail']` rather than widening
|
|
55
|
+
* to `ListenerName[]`, so `keyof typeof events` and `events['user:registered']`
|
|
56
|
+
* are exactly what the file declares.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* // app/Events.ts
|
|
61
|
+
* import { defineEvents } from '@stacksjs/events'
|
|
62
|
+
*
|
|
63
|
+
* export default defineEvents({
|
|
64
|
+
* 'user:registered': ['SendWelcomeEmail'],
|
|
65
|
+
* 'user:created': ['NotifyUser'],
|
|
66
|
+
* })
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* The `Record<Exclude<keyof T, EventName>, never>` half of the constraint is
|
|
70
|
+
* what rejects an event name that does not exist, and it is not redundant with
|
|
71
|
+
* `Events`. Excess-property checking is a freshness rule on the object literal,
|
|
72
|
+
* and it stops applying as soon as inference has something to work with: a map
|
|
73
|
+
* of one bad key is caught by it, and the same bad key sitting next to one good
|
|
74
|
+
* one is not, because `T` infers happily and a type with extra properties is
|
|
75
|
+
* assignable to a mapped type whose keys are all optional. Requiring every key
|
|
76
|
+
* outside `EventName` to hold something no listener list can be makes the check
|
|
77
|
+
* structural, so it holds however many entries the map has - and the shape it
|
|
78
|
+
* demands is an object with one impossible property, so the compiler's message
|
|
79
|
+
* is the sentence naming the fix rather than `not assignable to never`.
|
|
80
|
+
*/
|
|
81
|
+
export declare function defineEvents<const T extends Events & Record< Exclude<keyof T, EventName>, { 'this event name is not declared on AppEvents or AuthEvents': never } >,>(events: T): T;
|
|
45
82
|
// Singleton-friendly scope alias (#1878 E-5). Use to create a
|
|
46
83
|
// per-tenant / per-plugin wrapper that auto-prefixes event names.
|
|
47
84
|
export declare function scopedEvents(prefix: string): void;
|
|
@@ -140,6 +177,31 @@ export declare interface AuthEvents {
|
|
|
140
177
|
'user:password-reset': UserPasswordEvent
|
|
141
178
|
'user:password-changed': UserPasswordEvent
|
|
142
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* Augmentation target: every listener name `app/Events.ts` may reference.
|
|
182
|
+
*
|
|
183
|
+
* The keys are listener names as they are written in the map - `'NotifyUser'`,
|
|
184
|
+
* `'Auth/LoginAction'` - which is a name relative to `app/Listeners/` or
|
|
185
|
+
* `app/Actions/` (or the framework defaults behind them), without the
|
|
186
|
+
* extension. Filled by `storage/framework/types/registries.d.ts`, which reads
|
|
187
|
+
* the same name maps `resolveListener` resolves through - so the type and the
|
|
188
|
+
* resolution cannot disagree about what exists, because there is one list.
|
|
189
|
+
*
|
|
190
|
+
* An interface rather than a union alias because a union cannot be reopened,
|
|
191
|
+
* and this one has to be: the framework declares it empty and the application's
|
|
192
|
+
* generated declarations fill it in.
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```ts
|
|
196
|
+
* declare module '@stacksjs/events' {
|
|
197
|
+
* interface EventListeners {
|
|
198
|
+
* 'SendWelcomeEmail': true
|
|
199
|
+
* }
|
|
200
|
+
* }
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
203
|
+
// eslint-disable-next-line ts/no-empty-object-type -- augmentation target; empty by design
|
|
204
|
+
export declare interface EventListeners {}
|
|
143
205
|
/**
|
|
144
206
|
* Stacks event engine — a native, type-safe, async-aware pub/sub.
|
|
145
207
|
*
|
|
@@ -207,6 +269,37 @@ export type EventHandlerMap<Events extends EventMap> = Map<
|
|
|
207
269
|
* `AppEvents` and the typo becomes a compile error.
|
|
208
270
|
*/
|
|
209
271
|
export type StacksEvents = AppEvents & AuthEvents;
|
|
272
|
+
/**
|
|
273
|
+
* Every event name this application can dispatch or listen for.
|
|
274
|
+
*
|
|
275
|
+
* `& string` because `keyof` on an object type also yields `number | symbol`
|
|
276
|
+
* for an index signature, and an event name in `app/Events.ts` is a string key.
|
|
277
|
+
*/
|
|
278
|
+
export type EventName = keyof StacksEvents & string;
|
|
279
|
+
/**
|
|
280
|
+
* A listener name, as narrow as the application has made it.
|
|
281
|
+
*
|
|
282
|
+
* Falls back to `string` while `EventListeners` is empty. That is not a
|
|
283
|
+
* loophole left open: `generate:types` has not run yet in a project that has
|
|
284
|
+
* just been created, and rejecting every listener name until it does would make
|
|
285
|
+
* a fresh app fail to compile over a file it has never been told to generate.
|
|
286
|
+
* Once the registry exists, a misspelled listener is a compile error.
|
|
287
|
+
*/
|
|
288
|
+
export type ListenerName = keyof EventListeners extends never
|
|
289
|
+
? string
|
|
290
|
+
: keyof EventListeners & string;
|
|
291
|
+
/**
|
|
292
|
+
* The `app/Events.ts` map: an event name to the listeners that handle it.
|
|
293
|
+
*
|
|
294
|
+
* Both halves are checked. The key must be an event that exists - a name only
|
|
295
|
+
* `AppEvents` or `AuthEvents` declares - and the value must name listeners that
|
|
296
|
+
* are on disk. Neither used to be: the type was `{ [key: string]: string[] }`,
|
|
297
|
+
* which accepts `{ 'user:registerd': ['SendWelcomEmail'] }` in full, and both
|
|
298
|
+
* halves of that fail at runtime by doing nothing at all.
|
|
299
|
+
*/
|
|
300
|
+
export type Events = {
|
|
301
|
+
readonly [K in EventName]?: readonly ListenerName[]
|
|
302
|
+
}
|
|
210
303
|
declare type Dispatch = <Key extends keyof StacksEvents>(_type: Key, _event: StacksEvents[Key]) => void;
|
|
211
304
|
// eslint-disable-next-line pickier/no-unused-vars
|
|
212
305
|
declare type Listen = <Key extends keyof StacksEvents>(_type: Key, _handler: Handler<StacksEvents[Key]>, _options?: { priority?: number }) => void;
|
|
@@ -231,6 +324,6 @@ export {
|
|
|
231
324
|
// Boot-time listener auto-discovery (stacksjs/stacks#1878 E-3,
|
|
232
325
|
// closing F-3 from #1874). Scans `app/Listeners/**/*.ts` for
|
|
233
326
|
// default-exported `{ listensTo, handle }` modules and registers them.
|
|
234
|
-
export { discoverListeners, registerAppListeners, resetListenerRegistry } from './discover';
|
|
327
|
+
export { defineListener, discoverListeners, registerAppListeners, resetListenerRegistry, resetNameRegistries } from './discover';
|
|
235
328
|
// Default export keeps `import mitt from '@stacksjs/events'` shape working.
|
|
236
329
|
export default createEmitter;
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{existsSync as
|
|
2
|
+
import{existsSync as T,readdirSync as Y,statSync as G}from"fs";import{extname as J,join as k,resolve as q}from"path";import W from"process";function z(e){return e}async function C(e={}){let n=e.dir??k(W.cwd(),"app","Listeners"),t=e.extensions??[".ts",".js"],r={warn:e.log?.warn??((a)=>console.warn(a)),error:e.log?.error??((a)=>console.error(a)),info:e.log?.info??((a)=>console.info(a))};if(!T(n))return 0;let s=j(n,t);if(s.length===0)return 0;let o=0;for(let a of s)try{let u=await import(a),v=u?.default??u;if(typeof v==="function")continue;if(!Q(v)){r.warn(`[events/discover] ${a}: default export doesn't match ListenerModule shape ({ listensTo, handle }), skipping`);continue}for(let m of X(v.listensTo)){if(!D(m,a))continue;R(m,(x)=>v.handle(x,m)),o++}}catch(u){r.error(`[events/discover] failed to import ${a}: ${u instanceof Error?u.message:String(u)}`)}if(o>0)r.info(`[events/discover] registered ${o} listener${o===1?"":"s"} from ${n}`);return o}function j(e,n){let t=[],r=Y(e);for(let s of r){let o=k(e,s),a=G(o);if(a.isDirectory())t.push(...j(o,n));else if(a.isFile()&&n.includes(J(s)))t.push(o)}return t}function Q(e){let n=e?.listensTo;return!!e&&typeof e==="object"&&(typeof n==="string"||Array.isArray(n))&&typeof e.handle==="function"}function X(e){return(Array.isArray(e)?e:[e]).map((t)=>String(t).trim()).filter(Boolean)}var K=new Set;function D(e,n){let t=`${e}\x00${n}`;if(K.has(t))return!1;return K.add(t),!0}function Z(){K.clear()}async function ee(e={}){let n=e.base??W.cwd(),t={warn:e.log?.warn??((s)=>console.warn(s)),error:e.log?.error??((s)=>console.error(s)),info:e.log?.info??((s)=>console.info(s))},r=0;if(r+=await ne(n,t),r+=await C({...e,dir:e.dir??k(n,"app","Listeners"),log:{...t,info:()=>{}}}),r>0)t.info(`[events] registered ${r} listener${r===1?"":"s"}`);return r}async function ne(e,n){let t=["ts","js"].map((o)=>k(e,"app",`Events.${o}`)).find((o)=>T(o));if(!t)return 0;let r;try{let o=await import(t);r=o?.default??o}catch(o){return n.error(`[events] failed to read ${t}: ${o instanceof Error?o.message:String(o)}`),0}if(!r||typeof r!=="object")return 0;let s=0;for(let[o,a]of Object.entries(r))for(let u of Array.isArray(a)?a:[a]){if(typeof u!=="string"||!u.trim())continue;let v=await ie(e,u.trim());if(!v){n.warn(`[events] ${o}: no listener or action called ${u}`);continue}if(!D(o,v.id))continue;let{handle:m,validations:x}=v;R(o,(H)=>(te(o,u,H,x,n),m(H,o))),s++}return s}function te(e,n,t,r,s){if(!r||typeof t!=="object"||t===null)return;let o=t,a=[];for(let[u,v]of Object.entries(r)){let m=v?.rule?.validate;if(typeof m!=="function")continue;try{if(!m(o[u]).valid)a.push(u)}catch{a.push(u)}}if(a.length)s.warn(`[events] ${e}: payload does not match what ${n} declares (${a.join(", ")})`)}var P=new Map;async function O(e,n,t){let r=`${e}:${n}`,s=P.get(r);if(s)return s;let o=(async()=>{try{let a=k(e,"storage","framework","auto-imports"),v=(await import(k(a,`${n}.ts`)))[t];if(!v)return null;return Object.fromEntries(Object.entries(v).map(([m,x])=>[m,q(a,x)]))}catch{return null}})();return P.set(r,o),o}async function re(e,n){let t=await O(e,"listeners","listeners"),r=await O(e,"actions","actions");if(!t&&!r)return null;return t?.[n]??r?.[`Actions/${n}`]??null}function se(){P.clear()}function oe(e){let n=[k(e,"app"),k(e,"storage","framework","defaults","app")];try{let t=Bun.resolveSync("@stacksjs/defaults/package.json",e);n.push(k(t.slice(0,t.lastIndexOf("/")),"app"))}catch{}return n.filter((t)=>T(t))}async function ie(e,n){let t=[],r=await re(e,n);if(r)t.push(r);for(let s of oe(e))for(let o of["Listeners","Actions"])for(let a of["ts","js"])t.push(k(s,o,`${n}.${a}`));for(let s of t){if(!T(s))continue;try{let o=await import(s),a=o?.default??o,u=a?.handle;if(typeof u!=="function")continue;return{id:s,handle:u.bind(a),validations:a?.validations}}catch{continue}}return null}var U=Symbol.for("stacks.events.handler.error"),V=Symbol.for("stacks.events.handler.priority");function ae(e){if(e&&typeof e==="object"||typeof e==="function"){let n=e[V];return typeof n==="number"&&Number.isFinite(n)?n:0}return 0}function A(e){return e.map((n,t)=>({h:n,i:t,p:ae(n)})).sort((n,t)=>t.p-n.p||n.i-t.i).map((n)=>n.h)}function S(e,n,t){console.error(`[Events] ${e} for '${String(n)}':`,t)}function b(e){return!!e&&typeof e.catch==="function"}function M(e){let n=e??new Map,t=new Map,r=(i,f)=>{let d=t.get(i);if(!d)d=new RegExp(`^${i.replace(/\*/g,".*")}$`),t.set(i,d);return d.test(f)};function s(i,f,d){if(d?.priority!==void 0&&Number.isFinite(d.priority))f[V]=d.priority;let y=n.get(i);if(y)y.push(f);else n.set(i,[f])}function o(i,f){let d=(...y)=>(a(i,d),f(...y));d[U]=f,s(i,d)}function a(i,f){let d=n.get(i);if(!d)return;if(!f){n.set(i,[]);return}for(let y=d.length-1;y>=0;y--){let E=d[y];if(E===f||E?.[U]===f)d.splice(y,1)}}function u(i){if(i===void 0)n.clear();else n.delete(i)}function v(i){return n.get(i)?.length??0}function m(i,f){let d=n.get(i)?.slice(),y=n.get("*")?.slice(),E=d?A(d):void 0,w=y?A(y):void 0;if(E)for(let g of E)try{let c=g(f);if(b(c))c.catch((l)=>S("Async handler error",i,l))}catch(c){S("Handler error",i,c)}let p=String(i);if(n.forEach((g,c)=>{let l=String(c);if(l===p||l==="*"||!l.includes("*"))return;if(r(l,p))for(let _ of A(g.slice()))try{let L=_(i,f);if(b(L))L.catch((B)=>S(`Async pattern handler '${l}' error`,i,B))}catch(L){S(`Pattern handler '${l}' error`,i,L)}}),w)for(let g of w)try{let c=g(i,f);if(b(c))c.catch((l)=>S("Async wildcard handler error",i,l))}catch(c){S("Wildcard handler error",i,c)}}async function x(i,f){let d=[],y=async(p,g)=>{if(!p)return;for(let c of A(p.slice()))try{let l=g?c(i,f):c(f);d.push(b(l)?await l:l)}catch(l){S("Awaited handler error",i,l),d.push(void 0)}};await y(n.get(i),!1);let E=String(i),w=[];n.forEach((p,g)=>{let c=String(g);if(c===E||c==="*"||!c.includes("*"))return;if(r(c,E))w.push(c)});for(let p of w)await y(n.get(p),!0);return await y(n.get("*"),!0),d}async function H(i,f){let d=[],y=async(p,g)=>{if(!p)return;for(let c of A(p.slice()))try{let l=g?c(i,f):c(f),_=b(l)?await l:l;d.push({ok:!0,value:_})}catch(l){let _=l instanceof Error?l:Error(String(l));d.push({ok:!1,error:_})}};await y(n.get(i),!1);let E=String(i),w=[];n.forEach((p,g)=>{let c=String(g);if(c===E||c==="*"||!c.includes("*"))return;if(r(c,E))w.push(c)});for(let p of w)await y(n.get(p),!0);return await y(n.get("*"),!0),d}return{all:n,on:s,once:o,off:a,emit:m,emitAsync:x,emitAndCollect:H,removeAllListeners:u,listenerCount:v}}var pe=M,He=M;function ce(e,n){let t=(r)=>`${n}:${r}`;return{on(r,s,o){e.on(t(r),s,o)},once(r,s){e.once(t(r),s)},off(r,s){e.off(t(r),s)},emit(r,s){e.emit(t(r),s)},emitAsync(r,s){return e.emitAsync(t(r),s)},emitAndCollect(r,s){return e.emitAndCollect(t(r),s)},listenerCount(r){return e.listenerCount(t(r))}}}function ge(e){return e}var F=Symbol.for("stacks.events.emitter"),I=globalThis,N=I[F]??(I[F]=M()),h=N,me=N,de=h.emit,Ee=h.emitAsync,ke=h.emitAndCollect,we=de,he=h.all,R=h.on,xe=h.on,Se=h.once,_e=h.off;function Ae(e){return ce(N,e)}export{he as all,M as createEmitter,He as default,ge as defineEvents,z as defineListener,C as discoverListeners,de as dispatch,ke as dispatchAndCollect,Ee as dispatchAsync,h as emitter,N as events,R as listen,pe as mitt,_e as off,Se as once,ee as registerAppListeners,Z as resetListenerRegistry,se as resetNameRegistries,ce as scope,Ae as scopedEvents,we as useEvent,me as useEvents,xe as useListen};
|