|
@@ -3,7 +3,12 @@ import { Context } from './context.ts'
|
|
|
import { Fiber, FiberState } from './fiber.ts'
|
|
import { Fiber, FiberState } from './fiber.ts'
|
|
|
import { DisposableList, symbols } from './utils.ts'
|
|
import { DisposableList, symbols } from './utils.ts'
|
|
|
|
|
|
|
|
-/** Return whether an event result should stop a bail-style dispatch. */
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * Return whether an event result should stop a bail-style dispatch.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param value — a listener's return value.
|
|
|
|
|
+ * @returns `true` unless `value` is `null`, `false`, or `undefined`.
|
|
|
|
|
+ */
|
|
|
export function isBailed(value: any) {
|
|
export function isBailed(value: any) {
|
|
|
return value !== null && value !== false && value !== undefined
|
|
return value !== null && value !== false && value !== undefined
|
|
|
}
|
|
}
|
|
@@ -28,17 +33,75 @@ export type DispatchMode = 'emit' | 'parallel' | 'serial' | 'bail' | 'waterfall'
|
|
|
declare module './context.ts' {
|
|
declare module './context.ts' {
|
|
|
export interface Context {
|
|
export interface Context {
|
|
|
/* eslint-disable max-len */
|
|
/* eslint-disable max-len */
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Dispatch an event, running all listeners concurrently.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param name — the event name.
|
|
|
|
|
+ * @param args — arguments passed to every listener.
|
|
|
|
|
+ * @returns a promise resolving once every listener has settled.
|
|
|
|
|
+ */
|
|
|
parallel<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): Promise<void>
|
|
parallel<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): Promise<void>
|
|
|
|
|
+ /** Same as above, with an explicit `this` for listeners (also used for filtering). */
|
|
|
parallel<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): Promise<void>
|
|
parallel<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): Promise<void>
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Dispatch an event synchronously, ignoring listener return values.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param name — the event name.
|
|
|
|
|
+ * @param args — arguments passed to every listener.
|
|
|
|
|
+ */
|
|
|
emit<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): void
|
|
emit<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): void
|
|
|
|
|
+ /** Same as above, with an explicit `this` for listeners (also used for filtering). */
|
|
|
emit<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): void
|
|
emit<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): void
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Dispatch an event, awaiting listeners in order until one bails.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param name — the event name.
|
|
|
|
|
+ * @param args — arguments passed to each listener.
|
|
|
|
|
+ * @returns the first bail value (non-null, non-false, non-undefined), if any.
|
|
|
|
|
+ */
|
|
|
serial<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): Promisify<ReturnType<Events[K]>>
|
|
serial<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): Promisify<ReturnType<Events[K]>>
|
|
|
|
|
+ /** Same as above, with an explicit `this` for listeners (also used for filtering). */
|
|
|
serial<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): Promisify<ReturnType<Events[K]>>
|
|
serial<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): Promisify<ReturnType<Events[K]>>
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Dispatch an event, calling listeners in order until one bails.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param name — the event name.
|
|
|
|
|
+ * @param args — arguments passed to each listener.
|
|
|
|
|
+ * @returns the first bail value (non-null, non-false, non-undefined), if any.
|
|
|
|
|
+ */
|
|
|
bail<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
|
|
bail<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
|
|
|
|
|
+ /** Same as above, with an explicit `this` for listeners (also used for filtering). */
|
|
|
bail<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
|
|
bail<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Dispatch an event whose last argument is a `next` continuation.
|
|
|
|
|
+ *
|
|
|
|
|
+ * Each listener wraps the rest of the chain: calling `next()` invokes the
|
|
|
|
|
+ * next listener (finally the built-in behavior); not calling it vetoes.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param name — the event name.
|
|
|
|
|
+ * @param args — listener arguments; the final one is the innermost `next`.
|
|
|
|
|
+ * @returns the outermost listener's return value.
|
|
|
|
|
+ */
|
|
|
waterfall<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
|
|
waterfall<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
|
|
|
|
|
+ /** Same as above, with an explicit `this` for listeners (also used for filtering). */
|
|
|
waterfall<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
|
|
waterfall<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Register an event listener owned by the current fiber.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param name — the event name to listen for.
|
|
|
|
|
+ * @param listener — called with the dispatch arguments.
|
|
|
|
|
+ * @param options — listener options; a boolean is shorthand for `prepend`.
|
|
|
|
|
+ * @returns a disposer removing the listener; `true` if it was still registered.
|
|
|
|
|
+ */
|
|
|
on<K extends keyof Events>(name: K, listener: Events[K], options?: boolean | EventOptions): () => boolean
|
|
on<K extends keyof Events>(name: K, listener: Events[K], options?: boolean | EventOptions): () => boolean
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Same as `on()`, but the listener disposes itself after its first call.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param name — the event name to listen for.
|
|
|
|
|
+ * @param listener — called at most once with the dispatch arguments.
|
|
|
|
|
+ * @param options — listener options; a boolean is shorthand for `prepend`.
|
|
|
|
|
+ * @returns a disposer removing the listener; `true` if it was still registered.
|
|
|
|
|
+ */
|
|
|
once<K extends keyof Events>(name: K, listener: Events[K], options?: boolean | EventOptions): () => boolean
|
|
once<K extends keyof Events>(name: K, listener: Events[K], options?: boolean | EventOptions): () => boolean
|
|
|
/* eslint-enable max-len */
|
|
/* eslint-enable max-len */
|
|
|
}
|
|
}
|
|
@@ -91,7 +154,13 @@ export class EventsService {
|
|
|
}, { global: true, prepend: true })
|
|
}, { global: true, prepend: true })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** Resolve listeners for one dispatch and apply context filtering. */
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Resolve listeners for one dispatch and apply context filtering.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param type — the dispatch mode, reported on `internal/dispatch`.
|
|
|
|
|
+ * @param args — the raw dispatch arguments; consumed up to the event name.
|
|
|
|
|
+ * @returns the matching listener callbacks, bound to the dispatch `this`.
|
|
|
|
|
+ */
|
|
|
dispatch(type: string, args: any[]) {
|
|
dispatch(type: string, args: any[]) {
|
|
|
const thisArg = typeof args[0] === 'object' || typeof args[0] === 'function' ? args.shift() : null
|
|
const thisArg = typeof args[0] === 'object' || typeof args[0] === 'function' ? args.shift() : null
|
|
|
const name: string = args.shift()
|
|
const name: string = args.shift()
|
|
@@ -104,17 +173,31 @@ export class EventsService {
|
|
|
.map(hook => hook.callback.bind(thisArg))
|
|
.map(hook => hook.callback.bind(thisArg))
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** Run listeners concurrently and wait for all of them. */
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Run listeners concurrently and wait for all of them.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param args — optional `this`, the event name, then listener arguments.
|
|
|
|
|
+ * @returns a promise resolving once every listener has settled.
|
|
|
|
|
+ */
|
|
|
async parallel(...args: any[]) {
|
|
async parallel(...args: any[]) {
|
|
|
await Promise.all(this.dispatch('emit', args).map(cb => cb(...args)))
|
|
await Promise.all(this.dispatch('emit', args).map(cb => cb(...args)))
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** Run listeners synchronously without waiting for returned promises. */
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Run listeners synchronously without waiting for returned promises.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param args — optional `this`, the event name, then listener arguments.
|
|
|
|
|
+ */
|
|
|
emit(...args: any[]) {
|
|
emit(...args: any[]) {
|
|
|
this.dispatch('emit', args).map(cb => cb(...args))
|
|
this.dispatch('emit', args).map(cb => cb(...args))
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** Run listeners in order until one returns a bail value. */
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Run listeners in order, awaiting each, until one returns a bail value.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param args — optional `this`, the event name, then listener arguments.
|
|
|
|
|
+ * @returns the first bail value (see {@link isBailed}), if any.
|
|
|
|
|
+ */
|
|
|
async serial(...args: any[]) {
|
|
async serial(...args: any[]) {
|
|
|
for (const cb of this.dispatch('serial', args)) {
|
|
for (const cb of this.dispatch('serial', args)) {
|
|
|
const result = await cb(...args)
|
|
const result = await cb(...args)
|
|
@@ -122,7 +205,12 @@ export class EventsService {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** Run listeners synchronously until one returns a bail value. */
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Run listeners synchronously until one returns a bail value.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param args — optional `this`, the event name, then listener arguments.
|
|
|
|
|
+ * @returns the first bail value (see {@link isBailed}), if any.
|
|
|
|
|
+ */
|
|
|
bail(...args: any[]) {
|
|
bail(...args: any[]) {
|
|
|
for (const cb of this.dispatch('bail', args)) {
|
|
for (const cb of this.dispatch('bail', args)) {
|
|
|
const result = cb(...args)
|
|
const result = cb(...args)
|
|
@@ -130,7 +218,16 @@ export class EventsService {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** Compose listeners around the final `next` callback. */
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Compose listeners around the final `next` callback.
|
|
|
|
|
+ *
|
|
|
|
|
+ * The last dispatch argument is treated as the innermost `next`. Listeners
|
|
|
|
|
+ * run outermost-first; a listener that does not call `next()` vetoes the
|
|
|
|
|
+ * rest of the chain, including the built-in behavior.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param args — optional `this`, the event name, listener arguments, then `next`.
|
|
|
|
|
+ * @returns the outermost listener's return value.
|
|
|
|
|
+ */
|
|
|
waterfall(...args: any[]) {
|
|
waterfall(...args: any[]) {
|
|
|
const cbs = this.dispatch('waterfall', args)
|
|
const cbs = this.dispatch('waterfall', args)
|
|
|
const inner = args.pop()
|
|
const inner = args.pop()
|
|
@@ -142,6 +239,15 @@ export class EventsService {
|
|
|
return next()
|
|
return next()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Store a listener record as an effect on the current fiber.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param label — effect label shown in fiber diagnostics.
|
|
|
|
|
+ * @param hooks — the listener list for one event.
|
|
|
|
|
+ * @param callback — the listener to store.
|
|
|
|
|
+ * @param options — placement and filtering options.
|
|
|
|
|
+ * @returns a disposer that unregisters the listener.
|
|
|
|
|
+ */
|
|
|
register(label: string, hooks: Hook[], callback: any, options: EventOptions): () => void {
|
|
register(label: string, hooks: Hook[], callback: any, options: EventOptions): () => void {
|
|
|
const method = options.prepend ? 'unshift' : 'push'
|
|
const method = options.prepend ? 'unshift' : 'push'
|
|
|
return this.ctx.fiber.effect(() => {
|
|
return this.ctx.fiber.effect(() => {
|
|
@@ -150,6 +256,13 @@ export class EventsService {
|
|
|
}, label)
|
|
}, label)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Remove a stored listener record.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param hooks — the listener list for one event.
|
|
|
|
|
+ * @param callback — the listener to remove.
|
|
|
|
|
+ * @returns `true` if the listener was found and removed.
|
|
|
|
|
+ */
|
|
|
unregister(hooks: Hook[], callback: any) {
|
|
unregister(hooks: Hook[], callback: any) {
|
|
|
const index = hooks.findIndex(hook => hook.callback === callback)
|
|
const index = hooks.findIndex(hook => hook.callback === callback)
|
|
|
if (index >= 0) {
|
|
if (index >= 0) {
|
|
@@ -158,7 +271,17 @@ export class EventsService {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** Register an event listener owned by the current fiber. */
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Register an event listener owned by the current fiber.
|
|
|
|
|
+ *
|
|
|
|
|
+ * The listener is removed automatically when the fiber unloads. Throws
|
|
|
|
|
+ * `CordisError('INACTIVE_EFFECT')` if the fiber is already disposed.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param name — the event name to listen for.
|
|
|
|
|
+ * @param listener — called with the dispatch arguments.
|
|
|
|
|
+ * @param options — listener options; a boolean is shorthand for `prepend`.
|
|
|
|
|
+ * @returns a disposer removing the listener; `true` if it was still registered.
|
|
|
|
|
+ */
|
|
|
on(name: string | symbol, listener: (...args: any) => any, options?: boolean | EventOptions) {
|
|
on(name: string | symbol, listener: (...args: any) => any, options?: boolean | EventOptions) {
|
|
|
if (typeof options !== 'object') {
|
|
if (typeof options !== 'object') {
|
|
|
options = { prepend: options }
|
|
options = { prepend: options }
|
|
@@ -175,7 +298,14 @@ export class EventsService {
|
|
|
return this.register(label, hooks, listener, options)
|
|
return this.register(label, hooks, listener, options)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** Register an event listener that disposes itself after the first call. */
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Register an event listener that disposes itself after the first call.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param name — the event name to listen for.
|
|
|
|
|
+ * @param listener — called at most once with the dispatch arguments.
|
|
|
|
|
+ * @param options — listener options; a boolean is shorthand for `prepend`.
|
|
|
|
|
+ * @returns a disposer removing the listener; `true` if it was still registered.
|
|
|
|
|
+ */
|
|
|
once(name: string, listener: (...args: any) => any, options?: boolean | EventOptions) {
|
|
once(name: string, listener: (...args: any) => any, options?: boolean | EventOptions) {
|
|
|
const dispose = this.on(name, function (...args: any[]) {
|
|
const dispose = this.on(name, function (...args: any[]) {
|
|
|
dispose()
|
|
dispose()
|
|
@@ -194,12 +324,20 @@ export class EventsService {
|
|
|
* diagnostics before public events are delivered.
|
|
* diagnostics before public events are delivered.
|
|
|
*/
|
|
*/
|
|
|
export interface Events {
|
|
export interface Events {
|
|
|
|
|
+ /** A plugin fiber was created or its uid was cleared on disposal. */
|
|
|
'internal/plugin'(fiber: Fiber): void
|
|
'internal/plugin'(fiber: Fiber): void
|
|
|
|
|
+ /** A fiber changed lifecycle state; receives the fiber and its previous state. */
|
|
|
'internal/status'(fiber: Fiber, oldValue: FiberState): void
|
|
'internal/status'(fiber: Fiber, oldValue: FiberState): void
|
|
|
|
|
+ /** Interception hook for a service binding (no core producer). */
|
|
|
'internal/service'(this: Context, name: string, value: any): void
|
|
'internal/service'(this: Context, name: string, value: any): void
|
|
|
|
|
+ /** Waterfall: a fiber config update is being applied; skip `next()` to veto. */
|
|
|
'internal/update'(this: Fiber, config: any, noSave: boolean, next: () => void): void
|
|
'internal/update'(this: Fiber, config: any, noSave: boolean, next: () => void): void
|
|
|
|
|
+ /** Waterfall: a service is being read through the context proxy. */
|
|
|
'internal/get'(ctx: Context, name: string, error: Error, next: () => any): any
|
|
'internal/get'(ctx: Context, name: string, error: Error, next: () => any): any
|
|
|
|
|
+ /** Waterfall: a service is being written through the context proxy. */
|
|
|
'internal/set'(ctx: Context, name: string, value: any, error: Error, next: () => boolean): boolean
|
|
'internal/set'(ctx: Context, name: string, value: any, error: Error, next: () => boolean): boolean
|
|
|
|
|
+ /** Bail: a listener is being registered; a non-null result replaces registration. */
|
|
|
'internal/listener'(this: Context, name: string, listener: any, prepend: boolean): void
|
|
'internal/listener'(this: Context, name: string, listener: any, prepend: boolean): void
|
|
|
|
|
+ /** An event is being dispatched to listeners (fired for non-internal events only). */
|
|
|
'internal/dispatch'(mode: DispatchMode, name: string, args: any[], thisArg: any): void
|
|
'internal/dispatch'(mode: DispatchMode, name: string, args: any[], thisArg: any): void
|
|
|
}
|
|
}
|