Visitar URL original
feat(events): collect a before-event's answers where it is dispatched · stacksjs/stacks@4103dd1 · GitHub
Skip to content

Commit 4103dd1

Browse files
committed
feat(events): collect a before-event's answers where it is dispatched
dispatchAndCollect is asynchronous, so a model write that asks its before-listeners whether to go ahead had to wait a turn for them even when every one of them had already answered. dispatchAndCollectInPlace calls the listeners in the same order and returns their outcomes directly; it becomes a promise from the first listener that returns one, keeping the order.
1 parent b74c915 commit 4103dd1

2 files changed

Lines changed: 144 additions & 1 deletion

File tree

‎storage/framework/core/events/src/index.ts‎

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ export type EventHandlerMap<Events extends EventMap> = Map<
5252
EventHandlerList<Events[keyof Events]> | WildCardEventHandlerList<Events>
5353
>
5454

55+
/** What one handler made of an event: what it returned, or what it threw. */
56+
export type HandlerOutcome = { ok: true, value: unknown } | { ok: false, error: Error }
57+
5558
export interface Emitter<Events extends EventMap> {
5659
/** Underlying handler map. Mutating it directly is supported but rarely needed. */
5760
all: EventHandlerMap<Events>
@@ -98,6 +101,16 @@ export interface Emitter<Events extends EventMap> {
98101
*/
99102
emitAndCollect: <Key extends keyof Events>(_type: Key, _event: Events[Key]) => Promise<Array<{ ok: true, value: unknown } | { ok: false, error: Error }>>
100103

104+
/**
105+
* `emitAndCollect`, answering in place when it can: the outcomes themselves
106+
* when every handler returned a value, and a promise of them from the first
107+
* handler that returned a promise. Handlers run in the same order either
108+
* way, one after the other. For a caller that is itself synchronous unless
109+
* something makes it wait, such as a model write asking its `creating`
110+
* listeners whether to go ahead.
111+
*/
112+
emitAndCollectInPlace: <Key extends keyof Events>(_type: Key, _event: Events[Key]) => HandlerOutcome[] | Promise<HandlerOutcome[]>
113+
101114
/** Drop every handler for a type (or every handler everywhere when omitted). */
102115
removeAllListeners: (_type?: keyof Events | '*') => void
103116

@@ -383,7 +396,62 @@ export function createEmitter<Events extends EventMap>(
383396
return results
384397
}
385398

386-
return { all: map, on, once, off, emit, emitAsync, emitAndCollect, removeAllListeners, listenerCount } as Emitter<Events>
399+
function emitAndCollectInPlace(type: any, evt?: any): HandlerOutcome[] | Promise<HandlerOutcome[]> {
400+
// Every matching handler, in dispatch order: exact, then patterns, then '*'.
401+
const calls: Array<() => unknown> = []
402+
const queue = (handlers: Handler<any>[] | WildcardHandler<any>[] | undefined, isWildcard: boolean): void => {
403+
if (!handlers) return
404+
for (const handler of sortByPriority(priorities, handlers))
405+
calls.push(isWildcard ? () => (handler as WildcardHandler<any>)(type, evt) : () => (handler as Handler<any>)(evt))
406+
}
407+
queue(map.get(type) as Handler<any>[] | undefined, false)
408+
const typeStr = String(type)
409+
map.forEach((patternHandlers, key) => {
410+
const keyStr = String(key)
411+
if (keyStr === typeStr || keyStr === '*' || !keyStr.includes('*')) return
412+
if (matchPattern(keyStr, typeStr, patternHandlers)) queue(patternHandlers as WildcardHandler<any>[], true)
413+
})
414+
queue(map.get('*') as WildcardHandler<any>[] | undefined, true)
415+
416+
const outcomes: HandlerOutcome[] = []
417+
const failed = (err: unknown): HandlerOutcome => ({ ok: false, error: err instanceof Error ? err : new Error(String(err)) })
418+
419+
// From the first handler that has to be waited for, the rest are waited
420+
// for too, each after the one before it, as `emitAndCollect` does.
421+
const later = async (from: number, pending: Promise<unknown>): Promise<HandlerOutcome[]> => {
422+
try {
423+
outcomes.push({ ok: true, value: await pending })
424+
}
425+
catch (err) {
426+
outcomes.push(failed(err))
427+
}
428+
for (let index = from + 1; index < calls.length; index++) {
429+
try {
430+
const result = calls[index]!()
431+
outcomes.push({ ok: true, value: isPromiseLike(result) ? await result : result })
432+
}
433+
catch (err) {
434+
outcomes.push(failed(err))
435+
}
436+
}
437+
return outcomes
438+
}
439+
440+
for (let index = 0; index < calls.length; index++) {
441+
try {
442+
const result = calls[index]!()
443+
if (isPromiseLike(result))
444+
return later(index, result)
445+
outcomes.push({ ok: true, value: result })
446+
}
447+
catch (err) {
448+
outcomes.push(failed(err))
449+
}
450+
}
451+
return outcomes
452+
}
453+
454+
return { all: map, on, once, off, emit, emitAsync, emitAndCollect, emitAndCollectInPlace, removeAllListeners, listenerCount } as Emitter<Events>
387455
}
388456

389457
/**
@@ -696,6 +764,7 @@ type Listen = <Key extends keyof StacksEvents>(_type: Key, _handler: Handler<Sta
696764
type Off = <Key extends keyof StacksEvents>(_type: Key, handler?: Handler<StacksEvents[Key]>) => void
697765
type DispatchAsync = <Key extends keyof StacksEvents>(_type: Key, _event: StacksEvents[Key]) => Promise<unknown[]>
698766
type DispatchAndCollect = <Key extends keyof StacksEvents>(_type: Key, _event: StacksEvents[Key]) => Promise<Array<{ ok: true, value: unknown } | { ok: false, error: Error }>>
767+
type DispatchAndCollectInPlace = <Key extends keyof StacksEvents>(_type: Key, _event: StacksEvents[Key]) => HandlerOutcome[] | Promise<HandlerOutcome[]>
699768

700769
const emitter: Emitter<StacksEvents> = events
701770
const useEvents: Emitter<StacksEvents> = events
@@ -737,6 +806,11 @@ const dispatchAndCollect: DispatchAndCollect = (type, event) => {
737806
observe(type)
738807
return emitter.emitAndCollect(type, event)
739808
}
809+
/** `dispatchAndCollect`, answering in place when every listener did (see `emitAndCollectInPlace`). */
810+
const dispatchAndCollectInPlace: DispatchAndCollectInPlace = (type, event) => {
811+
observe(type)
812+
return emitter.emitAndCollectInPlace(type, event)
813+
}
740814
const useEvent: Dispatch = dispatch
741815
const all: EventHandlerMap<StacksEvents> = emitter.all
742816
const listen: Listen = emitter.on
@@ -748,6 +822,7 @@ export {
748822
all,
749823
dispatch,
750824
dispatchAndCollect,
825+
dispatchAndCollectInPlace,
751826
dispatchAsync,
752827
emitter,
753828
events,

‎storage/framework/core/events/tests/priority-and-collect.test.ts‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,71 @@ for (const method of ['emit', 'emitAsync', 'emitAndCollect'] as const) {
140140
if (method === 'emitAndCollect') expect(last).toEqual([{ ok: true, value: 'second' }])
141141
})
142142
}
143+
144+
describe('emitAndCollectInPlace', () => {
145+
test('answers in place when every handler did, in dispatch order', () => {
146+
const emitter = createEmitter<Record<string, number>>()
147+
const order: string[] = []
148+
emitter.on('thing', (value) => { order.push(`exact ${value}`); return undefined })
149+
emitter.on('thing', () => { order.push('refuses'); return false })
150+
emitter.on('th*', () => { order.push('pattern') })
151+
emitter.on('*', () => { order.push('wildcard') })
152+
153+
const outcomes = emitter.emitAndCollectInPlace('thing', 7)
154+
expect(Array.isArray(outcomes)).toBe(true)
155+
expect(order).toEqual(['exact 7', 'refuses', 'pattern', 'wildcard'])
156+
expect(outcomes).toEqual([
157+
{ ok: true, value: undefined },
158+
{ ok: true, value: false },
159+
{ ok: true, value: undefined },
160+
{ ok: true, value: undefined },
161+
])
162+
})
163+
164+
test('a handler that throws is an outcome, and the ones after it still run', () => {
165+
const emitter = createEmitter<Record<string, number>>()
166+
emitter.on('thing', () => { throw new Error('refused') })
167+
emitter.on('thing', () => 'still here')
168+
169+
const outcomes = emitter.emitAndCollectInPlace('thing', 1) as Array<{ ok: boolean, value?: unknown, error?: Error }>
170+
expect(outcomes.map(outcome => outcome.ok)).toEqual([false, true])
171+
expect(outcomes[0]!.error?.message).toBe('refused')
172+
expect(outcomes[1]!.value).toBe('still here')
173+
})
174+
175+
test('becomes a promise from the first handler that returns one, and keeps the order', async () => {
176+
const emitter = createEmitter<Record<string, number>>()
177+
const order: string[] = []
178+
emitter.on('thing', () => { order.push('first') })
179+
emitter.on('thing', async () => {
180+
await new Promise(resolve => setTimeout(resolve, 5))
181+
order.push('second, late')
182+
return false
183+
})
184+
emitter.on('thing', () => { order.push('third') })
185+
186+
const pending = emitter.emitAndCollectInPlace('thing', 1)
187+
expect(pending).toBeInstanceOf(Promise)
188+
// The third handler waits for the second, exactly as emitAndCollect has it.
189+
expect(order).toEqual(['first'])
190+
const outcomes = await pending
191+
expect(order).toEqual(['first', 'second, late', 'third'])
192+
expect(outcomes).toEqual([
193+
{ ok: true, value: undefined },
194+
{ ok: true, value: false },
195+
{ ok: true, value: undefined },
196+
])
197+
})
198+
199+
test('a rejection is an outcome too', async () => {
200+
const emitter = createEmitter<Record<string, number>>()
201+
emitter.on('thing', async () => { throw new Error('later') })
202+
const outcomes = await emitter.emitAndCollectInPlace('thing', 1) as Array<{ ok: boolean, error?: Error }>
203+
expect(outcomes[0]!.ok).toBe(false)
204+
expect(outcomes[0]!.error?.message).toBe('later')
205+
})
206+
207+
test('no handlers is an empty answer in place', () => {
208+
expect(createEmitter<Record<string, number>>().emitAndCollectInPlace('thing', 1)).toEqual([])
209+
})
210+
})

0 commit comments

Comments
 (0)