@stacksjs/events 0.72.57 → 0.72.61
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 +2 -0
- package/dist/index.d.ts +91 -8
- package/dist/index.js +1 -1
- package/package.json +2 -1
package/dist/discover.d.ts
CHANGED
|
@@ -56,3 +56,5 @@ declare interface DiscoverOptions {
|
|
|
56
56
|
declare interface RegisterOptions extends DiscoverOptions {
|
|
57
57
|
base?: string
|
|
58
58
|
}
|
|
59
|
+
/** An action's `validations:`, as much of it as this module needs. */
|
|
60
|
+
declare type PayloadValidations = Record<string, { rule?: { validate?: (_value: unknown) => { valid: boolean } } }>;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { ModelEvents } from '@stacksjs/types';
|
|
2
1
|
export type { ListenerModule } from './discover';
|
|
3
2
|
/**
|
|
4
3
|
* Create a fresh Stacks event emitter. Most consumers want the singleton
|
|
@@ -6,7 +5,7 @@ export type { ListenerModule } from './discover';
|
|
|
6
5
|
* (tests, child workers, plugin sandboxes).
|
|
7
6
|
*/
|
|
8
7
|
// eslint-disable-next-line pickier/no-unused-vars
|
|
9
|
-
export declare function createEmitter<Events extends
|
|
8
|
+
export declare function createEmitter<Events extends EventMap>(all?: EventHandlerMap<Events>): Emitter<Events>;
|
|
10
9
|
/**
|
|
11
10
|
* Build a scoped wrapper around an emitter (stacksjs/stacks#1878 E-5).
|
|
12
11
|
* Every dispatch and listen call is prefixed with `${prefix}:` so
|
|
@@ -34,7 +33,7 @@ export declare function createEmitter<Events extends Record<EventType, unknown>>
|
|
|
34
33
|
* // the prefix is mandatory.
|
|
35
34
|
* ```
|
|
36
35
|
*/
|
|
37
|
-
export declare function scope<Events extends
|
|
36
|
+
export declare function scope<Events extends EventMap>(underlying: Emitter<Events>, prefix: string): {
|
|
38
37
|
on: (type: string, handler: Handler<unknown>, options?: { priority?: number }) => void
|
|
39
38
|
once: (type: string, handler: Handler<unknown>) => void
|
|
40
39
|
off: (type: string, handler?: Handler<unknown>) => void
|
|
@@ -63,7 +62,7 @@ declare const listen: Listen;
|
|
|
63
62
|
declare const useListen: Listen;
|
|
64
63
|
declare const once: Listen;
|
|
65
64
|
declare const off: Off;
|
|
66
|
-
export declare interface Emitter<Events extends
|
|
65
|
+
export declare interface Emitter<Events extends EventMap> {
|
|
67
66
|
all: EventHandlerMap<Events>
|
|
68
67
|
on: (<Key extends keyof Events>(_type: Key, _handler: Handler<Events[Key]>, _options?: { priority?: number }) => void) &
|
|
69
68
|
((_type: '*', _handler: WildcardHandler<Events>, _options?: { priority?: number }) => void) &
|
|
@@ -103,27 +102,111 @@ export declare interface UserPasswordEvent {
|
|
|
103
102
|
id: number | string
|
|
104
103
|
email: string
|
|
105
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* What an application declares about its own events.
|
|
107
|
+
*
|
|
108
|
+
* Model events are the bulk of them and their payloads are the model rows,
|
|
109
|
+
* which only the application's own compilation can name. `@stacksjs/types`
|
|
110
|
+
* cannot carry them: it is reached by the ORM's type-test project, where naming
|
|
111
|
+
* a model drags all 97 model modules into a compilation that resolves
|
|
112
|
+
* `@stacksjs/orm` to a built dist and cannot compile them at all.
|
|
113
|
+
*
|
|
114
|
+
* So the precise map is generated into the application's declarations
|
|
115
|
+
* (`storage/framework/types/model-events.d.ts`, written by
|
|
116
|
+
* `buddy generate:types`) and augments this:
|
|
117
|
+
*
|
|
118
|
+
* ```ts
|
|
119
|
+
* declare module '@stacksjs/events' {
|
|
120
|
+
* interface AppEvents {
|
|
121
|
+
* 'user:created': ModelRow<typeof User>
|
|
122
|
+
* }
|
|
123
|
+
* }
|
|
124
|
+
* ```
|
|
125
|
+
*
|
|
126
|
+
* Declare your own events the same way. An application that declares nothing
|
|
127
|
+
* keeps exactly the behaviour it had before.
|
|
128
|
+
*/
|
|
129
|
+
// eslint-disable-next-line ts/no-empty-object-type -- augmentation target; empty by design
|
|
130
|
+
export declare interface AppEvents {}
|
|
106
131
|
/**
|
|
107
132
|
* Application-wide event types. Listeners and dispatchers below are
|
|
108
133
|
* pre-typed to this map; user-defined event names land here via
|
|
109
134
|
* `ModelEvents` (model-emitted events) + the explicit auth events listed.
|
|
110
135
|
*/
|
|
111
|
-
export declare interface
|
|
136
|
+
export declare interface AuthEvents {
|
|
112
137
|
'user:registered': UserRegisteredEvent
|
|
113
138
|
'user:logged-in': UserLoggedInEvent
|
|
114
139
|
'user:logged-out': UserLoggedOutEvent
|
|
115
140
|
'user:password-reset': UserPasswordEvent
|
|
116
141
|
'user:password-changed': UserPasswordEvent
|
|
117
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Stacks event engine — a native, type-safe, async-aware pub/sub.
|
|
145
|
+
*
|
|
146
|
+
* Originally adapted from `mitt`; rewritten in-house to:
|
|
147
|
+
* - Surface async handler errors on the same channel as sync ones
|
|
148
|
+
* (mitt swallowed unhandled rejections)
|
|
149
|
+
* - Support glob patterns (`user:*`, `*.created`) alongside `'*'` wildcard
|
|
150
|
+
* - Add `once`, `removeAllListeners`, `listenerCount` for parity with
|
|
151
|
+
* Node's EventEmitter ergonomics
|
|
152
|
+
* - Add `dispatchAsync` that AWAITS handlers and returns their results,
|
|
153
|
+
* so callers can express "fire this event AND wait until every
|
|
154
|
+
* listener finishes" (booking:cancelled → wait for refund + email
|
|
155
|
+
* before responding to the user)
|
|
156
|
+
*
|
|
157
|
+
* The legacy `mitt` export is preserved for backward compat — calling it
|
|
158
|
+
* gets you the same emitter you'd get from `createEmitter()`.
|
|
159
|
+
*/
|
|
118
160
|
export type EventType = string | symbol;
|
|
119
|
-
|
|
120
|
-
|
|
161
|
+
/**
|
|
162
|
+
* An event listener.
|
|
163
|
+
*
|
|
164
|
+
* `false` is part of the contract, not a leak: `dispatchBeforeEvent` in the ORM
|
|
165
|
+
* awaits every handler and cancels the write if any returned exactly `false` -
|
|
166
|
+
* which is how `'user:saving'` refuses a save. The type said `void`, so the
|
|
167
|
+
* documented way to cancel one did not compile and the only way to write it was
|
|
168
|
+
* to annotate around the type.
|
|
169
|
+
*
|
|
170
|
+
* Anything else a handler returns is ignored, as before.
|
|
171
|
+
*/
|
|
172
|
+
/**
|
|
173
|
+
* A map of event name to payload.
|
|
174
|
+
*
|
|
175
|
+
* `Record<EventType, unknown>` was the constraint, and it requires an index
|
|
176
|
+
* signature - which a precise event map deliberately does not have, because an
|
|
177
|
+
* index signature is exactly what made every misspelled event name legal. The
|
|
178
|
+
* constraint only ever needed the keys to be event names.
|
|
179
|
+
*/
|
|
180
|
+
export type EventMap = object;
|
|
181
|
+
export type Handler<T = unknown> = (_event: T) => void | false | Promise<void | false>;
|
|
182
|
+
/** The same contract as {@link Handler}, for a listener registered on `'*'`. */
|
|
183
|
+
export type WildcardHandler<T = Record<string, unknown>> = (_type: keyof T, _event: T[keyof T]) => void | false | Promise<void | false>;
|
|
121
184
|
export type EventHandlerList<T = unknown> = Array<Handler<T>>;
|
|
122
185
|
export type WildCardEventHandlerList<T = Record<string, unknown>> = Array<WildcardHandler<T>>;
|
|
123
|
-
export type EventHandlerMap<Events extends
|
|
186
|
+
export type EventHandlerMap<Events extends EventMap> = Map<
|
|
124
187
|
keyof Events | '*',
|
|
125
188
|
EventHandlerList<Events[keyof Events]> | WildCardEventHandlerList<Events>
|
|
126
189
|
>;
|
|
190
|
+
/**
|
|
191
|
+
* Application-wide event types, in precedence order.
|
|
192
|
+
*
|
|
193
|
+
* `ModelEvents` from `@stacksjs/types` is deliberately NOT in here. It was
|
|
194
|
+
* hand-maintained, and it named its events in kebab-case - `'cart-item:created'`
|
|
195
|
+
* - while `define-model.ts` dispatches `definition.name.toLowerCase()`, which
|
|
196
|
+
* for a model named `CartItem` is `'cartitem:created'`. Every compound-named
|
|
197
|
+
* model therefore had a documented, type-checked event that is never emitted:
|
|
198
|
+
* `listen('cart-item:created', …)` compiled and could not fire. `AppEvents` is
|
|
199
|
+
* derived from the models themselves and gets the name right, so keeping the
|
|
200
|
+
* old map in the union would only re-admit the 130-odd names that do not exist.
|
|
201
|
+
*
|
|
202
|
+
* There is no trailing index signature, deliberately. One used to be here, and
|
|
203
|
+
* it made every event name legal: `dispatch('user:creatd', …)` type-checked and
|
|
204
|
+
* reached nobody, which is the failure an event bus is most prone to and least
|
|
205
|
+
* able to report - a dispatch to a name nothing listens for looks exactly like
|
|
206
|
+
* a dispatch that had nothing to do. Declare an application's own events on
|
|
207
|
+
* `AppEvents` and the typo becomes a compile error.
|
|
208
|
+
*/
|
|
209
|
+
export type StacksEvents = AppEvents & AuthEvents;
|
|
127
210
|
declare type Dispatch = <Key extends keyof StacksEvents>(_type: Key, _event: StacksEvents[Key]) => void;
|
|
128
211
|
// eslint-disable-next-line pickier/no-unused-vars
|
|
129
212
|
declare type Listen = <Key extends keyof StacksEvents>(_type: Key, _handler: Handler<StacksEvents[Key]>, _options?: { priority?: number }) => void;
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{existsSync as
|
|
2
|
+
import{existsSync as P,readdirSync as V,statSync as B}from"fs";import{extname as Y,join as A}from"path";import W from"process";async function C(n={}){let e=n.dir??A(W.cwd(),"app","Listeners"),r=n.extensions??[".ts",".js"],t={warn:n.log?.warn??((c)=>console.warn(c)),error:n.log?.error??((c)=>console.error(c)),info:n.log?.info??((c)=>console.info(c))};if(!P(e))return 0;let o=O(e,r);if(o.length===0)return 0;let i=0;for(let c of o)try{let y=await import(c),p=y?.default??y;if(typeof p==="function")continue;if(!G(p)){t.warn(`[events/discover] ${c}: default export doesn't match ListenerModule shape ({ listensTo, handle }), skipping`);continue}for(let E of q(p.listensTo)){if(!D(E,c))continue;T(E,(_)=>p.handle(_,E)),i++}}catch(y){t.error(`[events/discover] failed to import ${c}: ${y instanceof Error?y.message:String(y)}`)}if(i>0)t.info(`[events/discover] registered ${i} listener${i===1?"":"s"} from ${e}`);return i}function O(n,e){let r=[],t=V(n);for(let o of t){let i=A(n,o),c=B(i);if(c.isDirectory())r.push(...O(i,e));else if(c.isFile()&&e.includes(Y(o)))r.push(i)}return r}function G(n){let e=n?.listensTo;return!!n&&typeof n==="object"&&(typeof e==="string"||Array.isArray(e))&&typeof n.handle==="function"}function q(n){return(Array.isArray(n)?n:[n]).map((r)=>String(r).trim()).filter(Boolean)}var L=new Set;function D(n,e){let r=`${n}\x00${e}`;if(L.has(r))return!1;return L.add(r),!0}function z(){L.clear()}async function J(n={}){let e=n.base??W.cwd(),r={warn:n.log?.warn??((o)=>console.warn(o)),error:n.log?.error??((o)=>console.error(o)),info:n.log?.info??((o)=>console.info(o))},t=0;if(t+=await Q(e,r),t+=await C({...n,dir:n.dir??A(e,"app","Listeners"),log:{...r,info:()=>{}}}),t>0)r.info(`[events] registered ${t} listener${t===1?"":"s"}`);return t}async function Q(n,e){let r=["ts","js"].map((i)=>A(n,"app",`Events.${i}`)).find((i)=>P(i));if(!r)return 0;let t;try{let i=await import(r);t=i?.default??i}catch(i){return e.error(`[events] failed to read ${r}: ${i instanceof Error?i.message:String(i)}`),0}if(!t||typeof t!=="object")return 0;let o=0;for(let[i,c]of Object.entries(t))for(let y of Array.isArray(c)?c:[c]){if(typeof y!=="string"||!y.trim())continue;let p=await Z(n,y.trim());if(!p){e.warn(`[events] ${i}: no listener or action called ${y}`);continue}if(!D(i,p.id))continue;let{handle:E,validations:_}=p;T(i,(S)=>(X(i,y,S,_,e),E(S,i))),o++}return o}function X(n,e,r,t,o){if(!t||typeof r!=="object"||r===null)return;let i=r,c=[];for(let[y,p]of Object.entries(t)){let E=p?.rule?.validate;if(typeof E!=="function")continue;try{if(!E(i[y]).valid)c.push(y)}catch{c.push(y)}}if(c.length)o.warn(`[events] ${n}: payload does not match what ${e} declares (${c.join(", ")})`)}async function Z(n,e){let r=[];for(let t of["Listeners","Actions"])for(let o of["ts","js"])r.push(A(n,"app",t,`${e}.${o}`));for(let t of r){if(!P(t))continue;try{let o=await import(t),i=o?.default??o,c=i?.handle;if(typeof c!=="function")continue;return{id:t,handle:c.bind(i),validations:i?.validations}}catch{continue}}return null}var j=Symbol.for("stacks.events.handler.error"),I=Symbol.for("stacks.events.handler.priority");function ee(n){if(n&&typeof n==="object"||typeof n==="function"){let e=n[I];return typeof e==="number"&&Number.isFinite(e)?e:0}return 0}function H(n){return n.map((e,r)=>({h:e,i:r,p:ee(e)})).sort((e,r)=>r.p-e.p||e.i-r.i).map((e)=>e.h)}function h(n,e,r){console.error(`[Events] ${n} for '${String(e)}':`,r)}function b(n){return!!n&&typeof n.catch==="function"}function R(n){let e=n??new Map,r=new Map,t=(s,f)=>{let d=r.get(s);if(!d)d=new RegExp(`^${s.replace(/\*/g,".*")}$`),r.set(s,d);return d.test(f)};function o(s,f,d){if(d?.priority!==void 0&&Number.isFinite(d.priority))f[I]=d.priority;let u=e.get(s);if(u)u.push(f);else e.set(s,[f])}function i(s,f){let d=(...u)=>(c(s,d),f(...u));d[j]=f,o(s,d)}function c(s,f){let d=e.get(s);if(!d)return;if(!f){e.set(s,[]);return}for(let u=d.length-1;u>=0;u--){let m=d[u];if(m===f||m?.[j]===f)d.splice(u,1)}}function y(s){if(s===void 0)e.clear();else e.delete(s)}function p(s){return e.get(s)?.length??0}function E(s,f){let d=e.get(s)?.slice(),u=e.get("*")?.slice(),m=d?H(d):void 0,k=u?H(u):void 0;if(m)for(let g of m)try{let a=g(f);if(b(a))a.catch((l)=>h("Async handler error",s,l))}catch(a){h("Handler error",s,a)}let v=String(s);if(e.forEach((g,a)=>{let l=String(a);if(l===v||l==="*"||!l.includes("*"))return;if(t(l,v))for(let x of H(g.slice()))try{let K=x(s,f);if(b(K))K.catch((N)=>h(`Async pattern handler '${l}' error`,s,N))}catch(K){h(`Pattern handler '${l}' error`,s,K)}}),k)for(let g of k)try{let a=g(s,f);if(b(a))a.catch((l)=>h("Async wildcard handler error",s,l))}catch(a){h("Wildcard handler error",s,a)}}async function _(s,f){let d=[],u=async(v,g)=>{if(!v)return;for(let a of H(v.slice()))try{let l=g?a(s,f):a(f);d.push(b(l)?await l:l)}catch(l){h("Awaited handler error",s,l),d.push(void 0)}};await u(e.get(s),!1);let m=String(s),k=[];e.forEach((v,g)=>{let a=String(g);if(a===m||a==="*"||!a.includes("*"))return;if(t(a,m))k.push(a)});for(let v of k)await u(e.get(v),!0);return await u(e.get("*"),!0),d}async function S(s,f){let d=[],u=async(v,g)=>{if(!v)return;for(let a of H(v.slice()))try{let l=g?a(s,f):a(f),x=b(l)?await l:l;d.push({ok:!0,value:x})}catch(l){let x=l instanceof Error?l:Error(String(l));d.push({ok:!1,error:x})}};await u(e.get(s),!1);let m=String(s),k=[];e.forEach((v,g)=>{let a=String(g);if(a===m||a==="*"||!a.includes("*"))return;if(t(a,m))k.push(a)});for(let v of k)await u(e.get(v),!0);return await u(e.get("*"),!0),d}return{all:e,on:o,once:i,off:c,emit:E,emitAsync:_,emitAndCollect:S,removeAllListeners:y,listenerCount:p}}var ce=R,ke=R;function ne(n,e){let r=(t)=>`${e}:${t}`;return{on(t,o,i){n.on(r(t),o,i)},once(t,o){n.once(r(t),o)},off(t,o){n.off(r(t),o)},emit(t,o){n.emit(r(t),o)},emitAsync(t,o){return n.emitAsync(r(t),o)},emitAndCollect(t,o){return n.emitAndCollect(r(t),o)},listenerCount(t){return n.listenerCount(r(t))}}}var U=Symbol.for("stacks.events.emitter"),F=globalThis,M=F[U]??(F[U]=R()),w=M,de=M,te=w.emit,le=w.emitAsync,fe=w.emitAndCollect,ue=te,ye=w.all,T=w.on,ve=w.on,pe=w.once,ge=w.off;function me(n){return ne(M,n)}export{ye as all,R as createEmitter,ke as default,C as discoverListeners,te as dispatch,fe as dispatchAndCollect,le as dispatchAsync,w as emitter,M as events,T as listen,ce as mitt,ge as off,pe as once,J as registerAppListeners,z as resetListenerRegistry,ne as scope,me as scopedEvents,ue as useEvent,de as useEvents,ve as useListen};
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/events",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.72.
|
|
5
|
+
"version": "0.72.61",
|
|
6
6
|
"description": "Functional event emitting.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"scripts": {
|
|
56
56
|
"build": "bun build.ts",
|
|
57
57
|
"typecheck": "bun tsc --noEmit",
|
|
58
|
+
"typecheck:types": "bun tsc --noEmit -p tsconfig.type-tests.json --pretty false",
|
|
58
59
|
"test": "bun test.ts",
|
|
59
60
|
"prepublishOnly": "bun run build"
|
|
60
61
|
},
|