@visisoft/staticland
Advanced tools
Comparing version
/* @license Apache-2.0 | ||
@visisoft/staticland v.2.0.0-alpha.3 visisoft.de | ||
(Build date: 10/24/2023 - 9:47:10 PM) | ||
@visisoft/staticland v.2.0.0 visisoft.de | ||
(Build date: 4/16/2024 - 6:25:26 PM) | ||
*/ | ||
@@ -121,539 +121,81 @@ 'use strict'; | ||
const anyMap = new WeakMap(); | ||
const eventsMap = new WeakMap(); | ||
const producersMap = new WeakMap(); | ||
/** | ||
* @template T | ||
* @typedef {{ resolve(t:T): void, reject(e: Error): void }} Sink | ||
*/ | ||
const anyProducer = Symbol('anyProducer'); | ||
const resolvedPromise = Promise.resolve(); | ||
/** | ||
* @template T | ||
* @typedef {{isSuccess: boolean, outcome: T|Error}} EmitterEvent | ||
*/ | ||
// Define symbols for "meta" events. | ||
const listenerAdded = Symbol('listenerAdded'); | ||
const listenerRemoved = Symbol('listenerRemoved'); | ||
/** | ||
* @template T | ||
* @typedef {{ emitOnce: (event: EmitterEvent<T>) => void, removeAllSinks: () => void, addSink: (sink: Sink<T>) => void, removeSink: (s: Sink<T>) => void }} Emitter | ||
*/ | ||
let canEmitMetaEvents = false; | ||
let isGlobalDebugEnabled = false; | ||
const | ||
/** | ||
* @template T | ||
* @param onLastSinkRemoved | ||
* @param onFirstSinkAdded | ||
* @return Emitter<T> | ||
*/ | ||
emitterForSingleEmission = ({ | ||
onLastSinkRemoved, | ||
onFirstSinkAdded | ||
}) => { | ||
const | ||
/** @type {Sink[]} */ | ||
sinks = [], | ||
function assertEventName(eventName) { | ||
if (typeof eventName !== 'string' && typeof eventName !== 'symbol' && typeof eventName !== 'number') { | ||
throw new TypeError('`eventName` must be a string, symbol, or number'); | ||
} | ||
} | ||
function assertListener(listener) { | ||
if (typeof listener !== 'function') { | ||
throw new TypeError('listener must be a function'); | ||
} | ||
} | ||
function getListeners(instance, eventName) { | ||
const events = eventsMap.get(instance); | ||
if (!events.has(eventName)) { | ||
return; | ||
} | ||
return events.get(eventName); | ||
} | ||
function getEventProducers(instance, eventName) { | ||
const key = typeof eventName === 'string' || typeof eventName === 'symbol' || typeof eventName === 'number' ? eventName : anyProducer; | ||
const producers = producersMap.get(instance); | ||
if (!producers.has(key)) { | ||
return; | ||
} | ||
return producers.get(key); | ||
} | ||
function enqueueProducers(instance, eventName, eventData) { | ||
const producers = producersMap.get(instance); | ||
if (producers.has(eventName)) { | ||
for (const producer of producers.get(eventName)) { | ||
producer.enqueue(eventData); | ||
} | ||
} | ||
if (producers.has(anyProducer)) { | ||
const item = Promise.all([eventName, eventData]); | ||
for (const producer of producers.get(anyProducer)) { | ||
producer.enqueue(item); | ||
} | ||
} | ||
} | ||
function iterator(instance, eventNames) { | ||
eventNames = Array.isArray(eventNames) ? eventNames : [eventNames]; | ||
let isFinished = false; | ||
let flush = () => {}; | ||
let queue = []; | ||
const producer = { | ||
enqueue(item) { | ||
queue.push(item); | ||
flush(); | ||
}, | ||
finish() { | ||
isFinished = true; | ||
flush(); | ||
}, | ||
}; | ||
for (const eventName of eventNames) { | ||
let set = getEventProducers(instance, eventName); | ||
if (!set) { | ||
set = new Set(); | ||
const producers = producersMap.get(instance); | ||
producers.set(eventName, set); | ||
} | ||
set.add(producer); | ||
} | ||
return { | ||
async next() { | ||
if (!queue) { | ||
return {done: true}; | ||
} | ||
if (queue.length === 0) { | ||
if (isFinished) { | ||
queue = undefined; | ||
return this.next(); | ||
/** | ||
* @template T | ||
* @param {Sink<T>} sink | ||
*/ | ||
addSink = sink => { | ||
sinks.push(sink); | ||
if (sinks.length === 1) { | ||
onFirstSinkAdded(); | ||
} | ||
}, | ||
await new Promise(resolve => { | ||
flush = resolve; | ||
}); | ||
return this.next(); | ||
} | ||
return { | ||
done: false, | ||
value: await queue.shift(), | ||
}; | ||
}, | ||
async return(value) { | ||
queue = undefined; | ||
for (const eventName of eventNames) { | ||
const set = getEventProducers(instance, eventName); | ||
if (set) { | ||
set.delete(producer); | ||
if (set.size === 0) { | ||
const producers = producersMap.get(instance); | ||
producers.delete(eventName); | ||
removeSink = sink => { | ||
const | ||
index = sinks.indexOf(sink); | ||
if (index !== -1) { | ||
sinks.splice(index, 1); | ||
if (sinks.length === 0) { | ||
onLastSinkRemoved(); | ||
} | ||
} | ||
} | ||
}, | ||
flush(); | ||
return arguments.length > 0 | ||
? {done: true, value: await value} | ||
: {done: true}; | ||
}, | ||
[Symbol.asyncIterator]() { | ||
return this; | ||
}, | ||
}; | ||
} | ||
function defaultMethodNamesOrAssert(methodNames) { | ||
if (methodNames === undefined) { | ||
return allEmitteryMethods; | ||
} | ||
if (!Array.isArray(methodNames)) { | ||
throw new TypeError('`methodNames` must be an array of strings'); | ||
} | ||
for (const methodName of methodNames) { | ||
if (!allEmitteryMethods.includes(methodName)) { | ||
if (typeof methodName !== 'string') { | ||
throw new TypeError('`methodNames` element must be a string'); | ||
} | ||
throw new Error(`${methodName} is not Emittery method`); | ||
} | ||
} | ||
return methodNames; | ||
} | ||
const isMetaEvent = eventName => eventName === listenerAdded || eventName === listenerRemoved; | ||
function emitMetaEvent(emitter, eventName, eventData) { | ||
if (isMetaEvent(eventName)) { | ||
try { | ||
canEmitMetaEvents = true; | ||
emitter.emit(eventName, eventData); | ||
} finally { | ||
canEmitMetaEvents = false; | ||
} | ||
} | ||
} | ||
class Emittery { | ||
static mixin(emitteryPropertyName, methodNames) { | ||
methodNames = defaultMethodNamesOrAssert(methodNames); | ||
return target => { | ||
if (typeof target !== 'function') { | ||
throw new TypeError('`target` must be function'); | ||
} | ||
for (const methodName of methodNames) { | ||
if (target.prototype[methodName] !== undefined) { | ||
throw new Error(`The property \`${methodName}\` already exists on \`target\``); | ||
removeAllSinks = () => { | ||
if (sinks.length > 0) { | ||
sinks.splice(0); | ||
onLastSinkRemoved(); | ||
} | ||
} | ||
}, | ||
function getEmitteryProperty() { | ||
Object.defineProperty(this, emitteryPropertyName, { | ||
enumerable: false, | ||
value: new Emittery(), | ||
/** | ||
* @template T | ||
* @param {EmitterEvent<T>} event | ||
*/ | ||
emitOnce = event => { | ||
const {isSuccess, outcome} = event; | ||
sinks.forEach(({resolve, reject}) => { | ||
(isSuccess ? resolve : reject)(outcome); | ||
}); | ||
return this[emitteryPropertyName]; | ||
} | ||
Object.defineProperty(target.prototype, emitteryPropertyName, { | ||
enumerable: false, | ||
get: getEmitteryProperty, | ||
}); | ||
const emitteryMethodCaller = methodName => function (...args) { | ||
return this[emitteryPropertyName][methodName](...args); | ||
removeAllSinks(); | ||
}; | ||
for (const methodName of methodNames) { | ||
Object.defineProperty(target.prototype, methodName, { | ||
enumerable: false, | ||
value: emitteryMethodCaller(methodName), | ||
}); | ||
} | ||
return target; | ||
return { | ||
addSink, | ||
removeSink, | ||
removeAllSinks, | ||
emitOnce | ||
}; | ||
} | ||
}; | ||
static get isDebugEnabled() { | ||
// In a browser environment, `globalThis.process` can potentially reference a DOM Element with a `#process` ID, | ||
// so instead of just type checking `globalThis.process`, we need to make sure that `globalThis.process.env` exists. | ||
// eslint-disable-next-line n/prefer-global/process | ||
if (typeof globalThis.process?.env !== 'object') { | ||
return isGlobalDebugEnabled; | ||
} | ||
// eslint-disable-next-line n/prefer-global/process | ||
const {env} = globalThis.process ?? {env: {}}; | ||
return env.DEBUG === 'emittery' || env.DEBUG === '*' || isGlobalDebugEnabled; | ||
} | ||
static set isDebugEnabled(newValue) { | ||
isGlobalDebugEnabled = newValue; | ||
} | ||
constructor(options = {}) { | ||
anyMap.set(this, new Set()); | ||
eventsMap.set(this, new Map()); | ||
producersMap.set(this, new Map()); | ||
producersMap.get(this).set(anyProducer, new Set()); | ||
this.debug = options.debug ?? {}; | ||
if (this.debug.enabled === undefined) { | ||
this.debug.enabled = false; | ||
} | ||
if (!this.debug.logger) { | ||
this.debug.logger = (type, debugName, eventName, eventData) => { | ||
try { | ||
// TODO: Use https://github.com/sindresorhus/safe-stringify when the package is more mature. Just copy-paste the code. | ||
eventData = JSON.stringify(eventData); | ||
} catch { | ||
eventData = `Object with the following keys failed to stringify: ${Object.keys(eventData).join(',')}`; | ||
} | ||
if (typeof eventName === 'symbol' || typeof eventName === 'number') { | ||
eventName = eventName.toString(); | ||
} | ||
const currentTime = new Date(); | ||
const logTime = `${currentTime.getHours()}:${currentTime.getMinutes()}:${currentTime.getSeconds()}.${currentTime.getMilliseconds()}`; | ||
console.log(`[${logTime}][emittery:${type}][${debugName}] Event Name: ${eventName}\n\tdata: ${eventData}`); | ||
}; | ||
} | ||
} | ||
logIfDebugEnabled(type, eventName, eventData) { | ||
if (Emittery.isDebugEnabled || this.debug.enabled) { | ||
this.debug.logger(type, this.debug.name, eventName, eventData); | ||
} | ||
} | ||
on(eventNames, listener) { | ||
assertListener(listener); | ||
eventNames = Array.isArray(eventNames) ? eventNames : [eventNames]; | ||
for (const eventName of eventNames) { | ||
assertEventName(eventName); | ||
let set = getListeners(this, eventName); | ||
if (!set) { | ||
set = new Set(); | ||
const events = eventsMap.get(this); | ||
events.set(eventName, set); | ||
} | ||
set.add(listener); | ||
this.logIfDebugEnabled('subscribe', eventName, undefined); | ||
if (!isMetaEvent(eventName)) { | ||
emitMetaEvent(this, listenerAdded, {eventName, listener}); | ||
} | ||
} | ||
return this.off.bind(this, eventNames, listener); | ||
} | ||
off(eventNames, listener) { | ||
assertListener(listener); | ||
eventNames = Array.isArray(eventNames) ? eventNames : [eventNames]; | ||
for (const eventName of eventNames) { | ||
assertEventName(eventName); | ||
const set = getListeners(this, eventName); | ||
if (set) { | ||
set.delete(listener); | ||
if (set.size === 0) { | ||
const events = eventsMap.get(this); | ||
events.delete(eventName); | ||
} | ||
} | ||
this.logIfDebugEnabled('unsubscribe', eventName, undefined); | ||
if (!isMetaEvent(eventName)) { | ||
emitMetaEvent(this, listenerRemoved, {eventName, listener}); | ||
} | ||
} | ||
} | ||
once(eventNames) { | ||
let off_; | ||
const promise = new Promise(resolve => { | ||
off_ = this.on(eventNames, data => { | ||
off_(); | ||
resolve(data); | ||
}); | ||
}); | ||
promise.off = off_; | ||
return promise; | ||
} | ||
events(eventNames) { | ||
eventNames = Array.isArray(eventNames) ? eventNames : [eventNames]; | ||
for (const eventName of eventNames) { | ||
assertEventName(eventName); | ||
} | ||
return iterator(this, eventNames); | ||
} | ||
async emit(eventName, eventData) { | ||
assertEventName(eventName); | ||
if (isMetaEvent(eventName) && !canEmitMetaEvents) { | ||
throw new TypeError('`eventName` cannot be meta event `listenerAdded` or `listenerRemoved`'); | ||
} | ||
this.logIfDebugEnabled('emit', eventName, eventData); | ||
enqueueProducers(this, eventName, eventData); | ||
const listeners = getListeners(this, eventName) ?? new Set(); | ||
const anyListeners = anyMap.get(this); | ||
const staticListeners = [...listeners]; | ||
const staticAnyListeners = isMetaEvent(eventName) ? [] : [...anyListeners]; | ||
await resolvedPromise; | ||
await Promise.all([ | ||
...staticListeners.map(async listener => { | ||
if (listeners.has(listener)) { | ||
return listener(eventData); | ||
} | ||
}), | ||
...staticAnyListeners.map(async listener => { | ||
if (anyListeners.has(listener)) { | ||
return listener(eventName, eventData); | ||
} | ||
}), | ||
]); | ||
} | ||
async emitSerial(eventName, eventData) { | ||
assertEventName(eventName); | ||
if (isMetaEvent(eventName) && !canEmitMetaEvents) { | ||
throw new TypeError('`eventName` cannot be meta event `listenerAdded` or `listenerRemoved`'); | ||
} | ||
this.logIfDebugEnabled('emitSerial', eventName, eventData); | ||
const listeners = getListeners(this, eventName) ?? new Set(); | ||
const anyListeners = anyMap.get(this); | ||
const staticListeners = [...listeners]; | ||
const staticAnyListeners = [...anyListeners]; | ||
await resolvedPromise; | ||
/* eslint-disable no-await-in-loop */ | ||
for (const listener of staticListeners) { | ||
if (listeners.has(listener)) { | ||
await listener(eventData); | ||
} | ||
} | ||
for (const listener of staticAnyListeners) { | ||
if (anyListeners.has(listener)) { | ||
await listener(eventName, eventData); | ||
} | ||
} | ||
/* eslint-enable no-await-in-loop */ | ||
} | ||
onAny(listener) { | ||
assertListener(listener); | ||
this.logIfDebugEnabled('subscribeAny', undefined, undefined); | ||
anyMap.get(this).add(listener); | ||
emitMetaEvent(this, listenerAdded, {listener}); | ||
return this.offAny.bind(this, listener); | ||
} | ||
anyEvent() { | ||
return iterator(this); | ||
} | ||
offAny(listener) { | ||
assertListener(listener); | ||
this.logIfDebugEnabled('unsubscribeAny', undefined, undefined); | ||
emitMetaEvent(this, listenerRemoved, {listener}); | ||
anyMap.get(this).delete(listener); | ||
} | ||
clearListeners(eventNames) { | ||
eventNames = Array.isArray(eventNames) ? eventNames : [eventNames]; | ||
for (const eventName of eventNames) { | ||
this.logIfDebugEnabled('clear', eventName, undefined); | ||
if (typeof eventName === 'string' || typeof eventName === 'symbol' || typeof eventName === 'number') { | ||
const set = getListeners(this, eventName); | ||
if (set) { | ||
set.clear(); | ||
} | ||
const producers = getEventProducers(this, eventName); | ||
if (producers) { | ||
for (const producer of producers) { | ||
producer.finish(); | ||
} | ||
producers.clear(); | ||
} | ||
} else { | ||
anyMap.get(this).clear(); | ||
for (const [eventName, listeners] of eventsMap.get(this).entries()) { | ||
listeners.clear(); | ||
eventsMap.get(this).delete(eventName); | ||
} | ||
for (const [eventName, producers] of producersMap.get(this).entries()) { | ||
for (const producer of producers) { | ||
producer.finish(); | ||
} | ||
producers.clear(); | ||
producersMap.get(this).delete(eventName); | ||
} | ||
} | ||
} | ||
} | ||
listenerCount(eventNames) { | ||
eventNames = Array.isArray(eventNames) ? eventNames : [eventNames]; | ||
let count = 0; | ||
for (const eventName of eventNames) { | ||
if (typeof eventName === 'string') { | ||
count += anyMap.get(this).size | ||
+ (getListeners(this, eventName)?.size ?? 0) | ||
+ (getEventProducers(this, eventName)?.size ?? 0) | ||
+ (getEventProducers(this)?.size ?? 0); | ||
continue; | ||
} | ||
if (typeof eventName !== 'undefined') { | ||
assertEventName(eventName); | ||
} | ||
count += anyMap.get(this).size; | ||
for (const value of eventsMap.get(this).values()) { | ||
count += value.size; | ||
} | ||
for (const value of producersMap.get(this).values()) { | ||
count += value.size; | ||
} | ||
} | ||
return count; | ||
} | ||
bindMethods(target, methodNames) { | ||
if (typeof target !== 'object' || target === null) { | ||
throw new TypeError('`target` must be an object'); | ||
} | ||
methodNames = defaultMethodNamesOrAssert(methodNames); | ||
for (const methodName of methodNames) { | ||
if (target[methodName] !== undefined) { | ||
throw new Error(`The property \`${methodName}\` already exists on \`target\``); | ||
} | ||
Object.defineProperty(target, methodName, { | ||
enumerable: false, | ||
value: this[methodName].bind(this), | ||
}); | ||
} | ||
} | ||
} | ||
const allEmitteryMethods = Object.getOwnPropertyNames(Emittery.prototype).filter(v => v !== 'constructor'); | ||
Object.defineProperty(Emittery, 'listenerAdded', { | ||
value: listenerAdded, | ||
writable: false, | ||
enumerable: true, | ||
configurable: false, | ||
}); | ||
Object.defineProperty(Emittery, 'listenerRemoved', { | ||
value: listenerRemoved, | ||
writable: false, | ||
enumerable: true, | ||
configurable: false, | ||
}); | ||
/** | ||
@@ -679,3 +221,3 @@ * StaticLand: deferred.js | ||
const | ||
emitter = new Emittery(), | ||
emitter = emitterForSingleEmission({onLastSinkRemoved: noop, onFirstSinkAdded: noop}), | ||
@@ -686,3 +228,3 @@ resolve = value => { | ||
// in case anybody already listens, tell them… | ||
emitter.emit("settle", {outcome: value, isSuccess: true}); | ||
emitter.emitOnce({outcome: value, isSuccess: true}); | ||
}, | ||
@@ -694,3 +236,3 @@ | ||
// in case anybody already listens, tell them… | ||
emitter.emit("settle", {outcome: error, isSuccess: false}); | ||
emitter.emitOnce({outcome: error, isSuccess: false}); | ||
}, | ||
@@ -701,28 +243,34 @@ | ||
// in case anybody listens, forget them all… | ||
emitter.clearListeners(["settle"]); | ||
emitter.removeAllSinks(); | ||
}, | ||
cancelable = (propagateResolve, propagateReject) => { | ||
const | ||
resolveAsap = ramda.thunkify(propagateResolve)(outcome), | ||
rejectAsap = ramda.thunkify(propagateReject)(outcome); | ||
switch (state) { | ||
case "pending": | ||
const | ||
unSubscribe = | ||
emitter.on("settle", ({ isSuccess, outcome: theOutcome }) => { | ||
(isSuccess ? propagateResolve : propagateReject)(theOutcome); | ||
unSubscribe(); | ||
}); | ||
sink = {resolve: propagateResolve, reject: propagateReject}, | ||
unsubscribe = () => { | ||
emitter.removeSink(sink); | ||
}; | ||
return unSubscribe; | ||
emitter.addSink(sink); | ||
return unsubscribe; | ||
case "cancelled": | ||
return noop; | ||
break; | ||
case "rejected": | ||
setTimeout(propagateReject, 0, outcome); | ||
return noop; | ||
queueMicrotask(rejectAsap); | ||
break; | ||
case "resolved": | ||
setTimeout(propagateResolve, 0, outcome); | ||
return noop; | ||
queueMicrotask(resolveAsap); | ||
break; | ||
default: | ||
setTimeout(propagateReject, 0, new Error(`Unexpected cancelable deferred state: "${state}"`)); | ||
return noop; | ||
queueMicrotask(() => { | ||
propagateReject(new Error(`Unexpected cancelable deferred state: "${state}"`)); | ||
}); | ||
} | ||
return noop; | ||
}; | ||
@@ -901,3 +449,2 @@ | ||
const | ||
emitter = new Emittery(), | ||
doNothing = () => undefined; | ||
@@ -910,44 +457,34 @@ | ||
isFinallyRejected = false, | ||
previousListenerCount = 0, | ||
finalOutcome; | ||
emitter.on(Emittery.listenerRemoved, (function() { | ||
return function onListenerRemoved() { | ||
//console.log(`listenerRemoved: count=${emitter.listenerCount("settle")}, previousCount=${previousListenerCount}`); | ||
if ((emitter.listenerCount("settle") === 0) && (previousListenerCount > 0) | ||
&& !isFinallyResolved && !isFinallyRejected) { | ||
const | ||
onLastSinkRemoved = () => { | ||
if (!isFinallyResolved && !isFinallyRejected) { | ||
// abort the running computation if the number of consumers drops to zero | ||
cancelRunningComputation(); | ||
} | ||
}, | ||
previousListenerCount = emitter.listenerCount("settle"); | ||
}; | ||
}())); | ||
// run once for all when the number of consumers exceeds zero | ||
onFirstSinkAdded = () => { | ||
cancelRunningComputation = cc( | ||
value => { | ||
finalOutcome = value; | ||
isFinallyResolved = true; | ||
emitter.emitOnce({outcome: value, isSuccess: true}); | ||
}, | ||
error => { | ||
finalOutcome = error; | ||
isFinallyRejected = true; | ||
emitter.emitOnce({outcome: error, isSuccess: false}); | ||
} | ||
); | ||
}, | ||
emitter.on(Emittery.listenerAdded, (function() { | ||
return function onListenerAdded() { | ||
//console.log(`listenerAdded: count=${emitter.listenerCount("settle")}, previousCount=${previousListenerCount}`); | ||
if ((emitter.listenerCount("settle") > 0) && (previousListenerCount === 0)) { | ||
// run once for all when the number of consumers exceeds zero | ||
cancelRunningComputation = cc( | ||
value => { | ||
finalOutcome = value; | ||
isFinallyResolved = true; | ||
emitter.emit("settle", {outcome: value, isSuccess: true}); | ||
}, | ||
error => { | ||
finalOutcome = error; | ||
isFinallyRejected = true; | ||
emitter.emit("settle", {outcome: error, isSuccess: false}); | ||
} | ||
); | ||
} | ||
emitter = emitterForSingleEmission({onFirstSinkAdded, onLastSinkRemoved}); | ||
previousListenerCount = emitter.listenerCount("settle"); | ||
}; | ||
}())); | ||
return (resolve, reject) => { | ||
if (isFinallyResolved) { | ||
setTimeout(resolve, 0, finalOutcome); | ||
const resolveAsap = ramda.thunkify(resolve)(finalOutcome); | ||
queueMicrotask(resolveAsap); | ||
return doNothing; | ||
@@ -957,3 +494,4 @@ } | ||
if (isFinallyRejected) { | ||
setTimeout(reject, 0, finalOutcome); | ||
const rejectAsap = ramda.thunkify(reject)(finalOutcome); | ||
queueMicrotask(rejectAsap); | ||
return doNothing; | ||
@@ -963,8 +501,7 @@ } | ||
const | ||
unConsume = | ||
emitter.on("settle", ({isSuccess, outcome}) => { | ||
(isSuccess ? resolve : reject)(outcome); | ||
unConsume(); | ||
}); | ||
sink = { resolve, reject }, | ||
unConsume = () => { emitter.removeSink(sink); }; | ||
emitter.addSink(sink); | ||
return unConsume; | ||
@@ -971,0 +508,0 @@ }; |
/* @license Apache-2.0 | ||
@visisoft/staticland v.2.0.0-alpha.3 visisoft.de | ||
(Build date: 10/24/2023 - 9:47:10 PM) | ||
@visisoft/staticland v.2.0.0 visisoft.de | ||
(Build date: 4/16/2024 - 6:25:26 PM) | ||
*/ | ||
@@ -5,0 +5,0 @@ 'use strict'; |
/* @license Apache-2.0 | ||
@visisoft/staticland v.2.0.0-alpha.3 visisoft.de | ||
(Build date: 10/24/2023 - 9:47:10 PM) | ||
@visisoft/staticland v.2.0.0 visisoft.de | ||
(Build date: 4/16/2024 - 6:25:26 PM) | ||
*/ | ||
@@ -5,0 +5,0 @@ 'use strict'; |
/* @license Apache-2.0 | ||
@visisoft/staticland v.2.0.0-alpha.3 visisoft.de | ||
(Build date: 10/24/2023 - 9:47:10 PM) | ||
@visisoft/staticland v.2.0.0 visisoft.de | ||
(Build date: 4/16/2024 - 6:25:26 PM) | ||
*/ | ||
@@ -5,0 +5,0 @@ 'use strict'; |
/* @license Apache-2.0 | ||
@visisoft/staticland v.2.0.0-alpha.3 visisoft.de | ||
(Build date: 10/24/2023 - 9:47:10 PM) | ||
@visisoft/staticland v.2.0.0 visisoft.de | ||
(Build date: 4/16/2024 - 6:25:26 PM) | ||
*/ | ||
@@ -5,0 +5,0 @@ 'use strict'; |
/* @license Apache-2.0 | ||
@visisoft/staticland v.2.0.0-alpha.3 visisoft.de | ||
(Build date: 10/24/2023 - 9:47:10 PM) | ||
@visisoft/staticland v.2.0.0 visisoft.de | ||
(Build date: 4/16/2024 - 6:25:26 PM) | ||
*/ | ||
@@ -5,0 +5,0 @@ 'use strict'; |
/* @license Apache-2.0 | ||
@visisoft/staticland v.2.0.0-alpha.3 visisoft.de | ||
(Build date: 10/24/2023 - 9:47:10 PM) | ||
@visisoft/staticland v.2.0.0 visisoft.de | ||
(Build date: 4/16/2024 - 6:25:26 PM) | ||
*/ | ||
@@ -5,0 +5,0 @@ 'use strict'; |
/* @license Apache-2.0 | ||
@visisoft/staticland v.2.0.0-alpha.3 visisoft.de | ||
(Build date: 10/24/2023 - 9:47:10 PM) | ||
@visisoft/staticland v.2.0.0 visisoft.de | ||
(Build date: 4/16/2024 - 6:25:26 PM) | ||
*/ | ||
@@ -1264,539 +1264,81 @@ 'use strict'; | ||
const anyMap = new WeakMap(); | ||
const eventsMap = new WeakMap(); | ||
const producersMap = new WeakMap(); | ||
/** | ||
* @template T | ||
* @typedef {{ resolve(t:T): void, reject(e: Error): void }} Sink | ||
*/ | ||
const anyProducer = Symbol('anyProducer'); | ||
const resolvedPromise = Promise.resolve(); | ||
/** | ||
* @template T | ||
* @typedef {{isSuccess: boolean, outcome: T|Error}} EmitterEvent | ||
*/ | ||
// Define symbols for "meta" events. | ||
const listenerAdded = Symbol('listenerAdded'); | ||
const listenerRemoved = Symbol('listenerRemoved'); | ||
/** | ||
* @template T | ||
* @typedef {{ emitOnce: (event: EmitterEvent<T>) => void, removeAllSinks: () => void, addSink: (sink: Sink<T>) => void, removeSink: (s: Sink<T>) => void }} Emitter | ||
*/ | ||
let canEmitMetaEvents = false; | ||
let isGlobalDebugEnabled = false; | ||
const | ||
/** | ||
* @template T | ||
* @param onLastSinkRemoved | ||
* @param onFirstSinkAdded | ||
* @return Emitter<T> | ||
*/ | ||
emitterForSingleEmission = ({ | ||
onLastSinkRemoved, | ||
onFirstSinkAdded | ||
}) => { | ||
const | ||
/** @type {Sink[]} */ | ||
sinks = [], | ||
function assertEventName(eventName) { | ||
if (typeof eventName !== 'string' && typeof eventName !== 'symbol' && typeof eventName !== 'number') { | ||
throw new TypeError('`eventName` must be a string, symbol, or number'); | ||
} | ||
} | ||
function assertListener(listener) { | ||
if (typeof listener !== 'function') { | ||
throw new TypeError('listener must be a function'); | ||
} | ||
} | ||
function getListeners(instance, eventName) { | ||
const events = eventsMap.get(instance); | ||
if (!events.has(eventName)) { | ||
return; | ||
} | ||
return events.get(eventName); | ||
} | ||
function getEventProducers(instance, eventName) { | ||
const key = typeof eventName === 'string' || typeof eventName === 'symbol' || typeof eventName === 'number' ? eventName : anyProducer; | ||
const producers = producersMap.get(instance); | ||
if (!producers.has(key)) { | ||
return; | ||
} | ||
return producers.get(key); | ||
} | ||
function enqueueProducers(instance, eventName, eventData) { | ||
const producers = producersMap.get(instance); | ||
if (producers.has(eventName)) { | ||
for (const producer of producers.get(eventName)) { | ||
producer.enqueue(eventData); | ||
} | ||
} | ||
if (producers.has(anyProducer)) { | ||
const item = Promise.all([eventName, eventData]); | ||
for (const producer of producers.get(anyProducer)) { | ||
producer.enqueue(item); | ||
} | ||
} | ||
} | ||
function iterator(instance, eventNames) { | ||
eventNames = Array.isArray(eventNames) ? eventNames : [eventNames]; | ||
let isFinished = false; | ||
let flush = () => {}; | ||
let queue = []; | ||
const producer = { | ||
enqueue(item) { | ||
queue.push(item); | ||
flush(); | ||
}, | ||
finish() { | ||
isFinished = true; | ||
flush(); | ||
}, | ||
}; | ||
for (const eventName of eventNames) { | ||
let set = getEventProducers(instance, eventName); | ||
if (!set) { | ||
set = new Set(); | ||
const producers = producersMap.get(instance); | ||
producers.set(eventName, set); | ||
} | ||
set.add(producer); | ||
} | ||
return { | ||
async next() { | ||
if (!queue) { | ||
return {done: true}; | ||
} | ||
if (queue.length === 0) { | ||
if (isFinished) { | ||
queue = undefined; | ||
return this.next(); | ||
/** | ||
* @template T | ||
* @param {Sink<T>} sink | ||
*/ | ||
addSink = sink => { | ||
sinks.push(sink); | ||
if (sinks.length === 1) { | ||
onFirstSinkAdded(); | ||
} | ||
}, | ||
await new Promise(resolve => { | ||
flush = resolve; | ||
}); | ||
return this.next(); | ||
} | ||
return { | ||
done: false, | ||
value: await queue.shift(), | ||
}; | ||
}, | ||
async return(value) { | ||
queue = undefined; | ||
for (const eventName of eventNames) { | ||
const set = getEventProducers(instance, eventName); | ||
if (set) { | ||
set.delete(producer); | ||
if (set.size === 0) { | ||
const producers = producersMap.get(instance); | ||
producers.delete(eventName); | ||
removeSink = sink => { | ||
const | ||
index = sinks.indexOf(sink); | ||
if (index !== -1) { | ||
sinks.splice(index, 1); | ||
if (sinks.length === 0) { | ||
onLastSinkRemoved(); | ||
} | ||
} | ||
} | ||
}, | ||
flush(); | ||
return arguments.length > 0 | ||
? {done: true, value: await value} | ||
: {done: true}; | ||
}, | ||
[Symbol.asyncIterator]() { | ||
return this; | ||
}, | ||
}; | ||
} | ||
function defaultMethodNamesOrAssert(methodNames) { | ||
if (methodNames === undefined) { | ||
return allEmitteryMethods; | ||
} | ||
if (!Array.isArray(methodNames)) { | ||
throw new TypeError('`methodNames` must be an array of strings'); | ||
} | ||
for (const methodName of methodNames) { | ||
if (!allEmitteryMethods.includes(methodName)) { | ||
if (typeof methodName !== 'string') { | ||
throw new TypeError('`methodNames` element must be a string'); | ||
} | ||
throw new Error(`${methodName} is not Emittery method`); | ||
} | ||
} | ||
return methodNames; | ||
} | ||
const isMetaEvent = eventName => eventName === listenerAdded || eventName === listenerRemoved; | ||
function emitMetaEvent(emitter, eventName, eventData) { | ||
if (isMetaEvent(eventName)) { | ||
try { | ||
canEmitMetaEvents = true; | ||
emitter.emit(eventName, eventData); | ||
} finally { | ||
canEmitMetaEvents = false; | ||
} | ||
} | ||
} | ||
class Emittery { | ||
static mixin(emitteryPropertyName, methodNames) { | ||
methodNames = defaultMethodNamesOrAssert(methodNames); | ||
return target => { | ||
if (typeof target !== 'function') { | ||
throw new TypeError('`target` must be function'); | ||
} | ||
for (const methodName of methodNames) { | ||
if (target.prototype[methodName] !== undefined) { | ||
throw new Error(`The property \`${methodName}\` already exists on \`target\``); | ||
removeAllSinks = () => { | ||
if (sinks.length > 0) { | ||
sinks.splice(0); | ||
onLastSinkRemoved(); | ||
} | ||
} | ||
}, | ||
function getEmitteryProperty() { | ||
Object.defineProperty(this, emitteryPropertyName, { | ||
enumerable: false, | ||
value: new Emittery(), | ||
/** | ||
* @template T | ||
* @param {EmitterEvent<T>} event | ||
*/ | ||
emitOnce = event => { | ||
const {isSuccess, outcome} = event; | ||
sinks.forEach(({resolve, reject}) => { | ||
(isSuccess ? resolve : reject)(outcome); | ||
}); | ||
return this[emitteryPropertyName]; | ||
} | ||
Object.defineProperty(target.prototype, emitteryPropertyName, { | ||
enumerable: false, | ||
get: getEmitteryProperty, | ||
}); | ||
const emitteryMethodCaller = methodName => function (...args) { | ||
return this[emitteryPropertyName][methodName](...args); | ||
removeAllSinks(); | ||
}; | ||
for (const methodName of methodNames) { | ||
Object.defineProperty(target.prototype, methodName, { | ||
enumerable: false, | ||
value: emitteryMethodCaller(methodName), | ||
}); | ||
} | ||
return target; | ||
return { | ||
addSink, | ||
removeSink, | ||
removeAllSinks, | ||
emitOnce | ||
}; | ||
} | ||
}; | ||
static get isDebugEnabled() { | ||
// In a browser environment, `globalThis.process` can potentially reference a DOM Element with a `#process` ID, | ||
// so instead of just type checking `globalThis.process`, we need to make sure that `globalThis.process.env` exists. | ||
// eslint-disable-next-line n/prefer-global/process | ||
if (typeof globalThis.process?.env !== 'object') { | ||
return isGlobalDebugEnabled; | ||
} | ||
// eslint-disable-next-line n/prefer-global/process | ||
const {env} = globalThis.process ?? {env: {}}; | ||
return env.DEBUG === 'emittery' || env.DEBUG === '*' || isGlobalDebugEnabled; | ||
} | ||
static set isDebugEnabled(newValue) { | ||
isGlobalDebugEnabled = newValue; | ||
} | ||
constructor(options = {}) { | ||
anyMap.set(this, new Set()); | ||
eventsMap.set(this, new Map()); | ||
producersMap.set(this, new Map()); | ||
producersMap.get(this).set(anyProducer, new Set()); | ||
this.debug = options.debug ?? {}; | ||
if (this.debug.enabled === undefined) { | ||
this.debug.enabled = false; | ||
} | ||
if (!this.debug.logger) { | ||
this.debug.logger = (type, debugName, eventName, eventData) => { | ||
try { | ||
// TODO: Use https://github.com/sindresorhus/safe-stringify when the package is more mature. Just copy-paste the code. | ||
eventData = JSON.stringify(eventData); | ||
} catch { | ||
eventData = `Object with the following keys failed to stringify: ${Object.keys(eventData).join(',')}`; | ||
} | ||
if (typeof eventName === 'symbol' || typeof eventName === 'number') { | ||
eventName = eventName.toString(); | ||
} | ||
const currentTime = new Date(); | ||
const logTime = `${currentTime.getHours()}:${currentTime.getMinutes()}:${currentTime.getSeconds()}.${currentTime.getMilliseconds()}`; | ||
console.log(`[${logTime}][emittery:${type}][${debugName}] Event Name: ${eventName}\n\tdata: ${eventData}`); | ||
}; | ||
} | ||
} | ||
logIfDebugEnabled(type, eventName, eventData) { | ||
if (Emittery.isDebugEnabled || this.debug.enabled) { | ||
this.debug.logger(type, this.debug.name, eventName, eventData); | ||
} | ||
} | ||
on(eventNames, listener) { | ||
assertListener(listener); | ||
eventNames = Array.isArray(eventNames) ? eventNames : [eventNames]; | ||
for (const eventName of eventNames) { | ||
assertEventName(eventName); | ||
let set = getListeners(this, eventName); | ||
if (!set) { | ||
set = new Set(); | ||
const events = eventsMap.get(this); | ||
events.set(eventName, set); | ||
} | ||
set.add(listener); | ||
this.logIfDebugEnabled('subscribe', eventName, undefined); | ||
if (!isMetaEvent(eventName)) { | ||
emitMetaEvent(this, listenerAdded, {eventName, listener}); | ||
} | ||
} | ||
return this.off.bind(this, eventNames, listener); | ||
} | ||
off(eventNames, listener) { | ||
assertListener(listener); | ||
eventNames = Array.isArray(eventNames) ? eventNames : [eventNames]; | ||
for (const eventName of eventNames) { | ||
assertEventName(eventName); | ||
const set = getListeners(this, eventName); | ||
if (set) { | ||
set.delete(listener); | ||
if (set.size === 0) { | ||
const events = eventsMap.get(this); | ||
events.delete(eventName); | ||
} | ||
} | ||
this.logIfDebugEnabled('unsubscribe', eventName, undefined); | ||
if (!isMetaEvent(eventName)) { | ||
emitMetaEvent(this, listenerRemoved, {eventName, listener}); | ||
} | ||
} | ||
} | ||
once(eventNames) { | ||
let off_; | ||
const promise = new Promise(resolve => { | ||
off_ = this.on(eventNames, data => { | ||
off_(); | ||
resolve(data); | ||
}); | ||
}); | ||
promise.off = off_; | ||
return promise; | ||
} | ||
events(eventNames) { | ||
eventNames = Array.isArray(eventNames) ? eventNames : [eventNames]; | ||
for (const eventName of eventNames) { | ||
assertEventName(eventName); | ||
} | ||
return iterator(this, eventNames); | ||
} | ||
async emit(eventName, eventData) { | ||
assertEventName(eventName); | ||
if (isMetaEvent(eventName) && !canEmitMetaEvents) { | ||
throw new TypeError('`eventName` cannot be meta event `listenerAdded` or `listenerRemoved`'); | ||
} | ||
this.logIfDebugEnabled('emit', eventName, eventData); | ||
enqueueProducers(this, eventName, eventData); | ||
const listeners = getListeners(this, eventName) ?? new Set(); | ||
const anyListeners = anyMap.get(this); | ||
const staticListeners = [...listeners]; | ||
const staticAnyListeners = isMetaEvent(eventName) ? [] : [...anyListeners]; | ||
await resolvedPromise; | ||
await Promise.all([ | ||
...staticListeners.map(async listener => { | ||
if (listeners.has(listener)) { | ||
return listener(eventData); | ||
} | ||
}), | ||
...staticAnyListeners.map(async listener => { | ||
if (anyListeners.has(listener)) { | ||
return listener(eventName, eventData); | ||
} | ||
}), | ||
]); | ||
} | ||
async emitSerial(eventName, eventData) { | ||
assertEventName(eventName); | ||
if (isMetaEvent(eventName) && !canEmitMetaEvents) { | ||
throw new TypeError('`eventName` cannot be meta event `listenerAdded` or `listenerRemoved`'); | ||
} | ||
this.logIfDebugEnabled('emitSerial', eventName, eventData); | ||
const listeners = getListeners(this, eventName) ?? new Set(); | ||
const anyListeners = anyMap.get(this); | ||
const staticListeners = [...listeners]; | ||
const staticAnyListeners = [...anyListeners]; | ||
await resolvedPromise; | ||
/* eslint-disable no-await-in-loop */ | ||
for (const listener of staticListeners) { | ||
if (listeners.has(listener)) { | ||
await listener(eventData); | ||
} | ||
} | ||
for (const listener of staticAnyListeners) { | ||
if (anyListeners.has(listener)) { | ||
await listener(eventName, eventData); | ||
} | ||
} | ||
/* eslint-enable no-await-in-loop */ | ||
} | ||
onAny(listener) { | ||
assertListener(listener); | ||
this.logIfDebugEnabled('subscribeAny', undefined, undefined); | ||
anyMap.get(this).add(listener); | ||
emitMetaEvent(this, listenerAdded, {listener}); | ||
return this.offAny.bind(this, listener); | ||
} | ||
anyEvent() { | ||
return iterator(this); | ||
} | ||
offAny(listener) { | ||
assertListener(listener); | ||
this.logIfDebugEnabled('unsubscribeAny', undefined, undefined); | ||
emitMetaEvent(this, listenerRemoved, {listener}); | ||
anyMap.get(this).delete(listener); | ||
} | ||
clearListeners(eventNames) { | ||
eventNames = Array.isArray(eventNames) ? eventNames : [eventNames]; | ||
for (const eventName of eventNames) { | ||
this.logIfDebugEnabled('clear', eventName, undefined); | ||
if (typeof eventName === 'string' || typeof eventName === 'symbol' || typeof eventName === 'number') { | ||
const set = getListeners(this, eventName); | ||
if (set) { | ||
set.clear(); | ||
} | ||
const producers = getEventProducers(this, eventName); | ||
if (producers) { | ||
for (const producer of producers) { | ||
producer.finish(); | ||
} | ||
producers.clear(); | ||
} | ||
} else { | ||
anyMap.get(this).clear(); | ||
for (const [eventName, listeners] of eventsMap.get(this).entries()) { | ||
listeners.clear(); | ||
eventsMap.get(this).delete(eventName); | ||
} | ||
for (const [eventName, producers] of producersMap.get(this).entries()) { | ||
for (const producer of producers) { | ||
producer.finish(); | ||
} | ||
producers.clear(); | ||
producersMap.get(this).delete(eventName); | ||
} | ||
} | ||
} | ||
} | ||
listenerCount(eventNames) { | ||
eventNames = Array.isArray(eventNames) ? eventNames : [eventNames]; | ||
let count = 0; | ||
for (const eventName of eventNames) { | ||
if (typeof eventName === 'string') { | ||
count += anyMap.get(this).size | ||
+ (getListeners(this, eventName)?.size ?? 0) | ||
+ (getEventProducers(this, eventName)?.size ?? 0) | ||
+ (getEventProducers(this)?.size ?? 0); | ||
continue; | ||
} | ||
if (typeof eventName !== 'undefined') { | ||
assertEventName(eventName); | ||
} | ||
count += anyMap.get(this).size; | ||
for (const value of eventsMap.get(this).values()) { | ||
count += value.size; | ||
} | ||
for (const value of producersMap.get(this).values()) { | ||
count += value.size; | ||
} | ||
} | ||
return count; | ||
} | ||
bindMethods(target, methodNames) { | ||
if (typeof target !== 'object' || target === null) { | ||
throw new TypeError('`target` must be an object'); | ||
} | ||
methodNames = defaultMethodNamesOrAssert(methodNames); | ||
for (const methodName of methodNames) { | ||
if (target[methodName] !== undefined) { | ||
throw new Error(`The property \`${methodName}\` already exists on \`target\``); | ||
} | ||
Object.defineProperty(target, methodName, { | ||
enumerable: false, | ||
value: this[methodName].bind(this), | ||
}); | ||
} | ||
} | ||
} | ||
const allEmitteryMethods = Object.getOwnPropertyNames(Emittery.prototype).filter(v => v !== 'constructor'); | ||
Object.defineProperty(Emittery, 'listenerAdded', { | ||
value: listenerAdded, | ||
writable: false, | ||
enumerable: true, | ||
configurable: false, | ||
}); | ||
Object.defineProperty(Emittery, 'listenerRemoved', { | ||
value: listenerRemoved, | ||
writable: false, | ||
enumerable: true, | ||
configurable: false, | ||
}); | ||
/** | ||
@@ -1822,3 +1364,3 @@ * StaticLand: deferred.js | ||
const | ||
emitter = new Emittery(), | ||
emitter = emitterForSingleEmission({onLastSinkRemoved: noop$1, onFirstSinkAdded: noop$1}), | ||
@@ -1829,3 +1371,3 @@ resolve = value => { | ||
// in case anybody already listens, tell them… | ||
emitter.emit("settle", {outcome: value, isSuccess: true}); | ||
emitter.emitOnce({outcome: value, isSuccess: true}); | ||
}, | ||
@@ -1837,3 +1379,3 @@ | ||
// in case anybody already listens, tell them… | ||
emitter.emit("settle", {outcome: error, isSuccess: false}); | ||
emitter.emitOnce({outcome: error, isSuccess: false}); | ||
}, | ||
@@ -1844,28 +1386,34 @@ | ||
// in case anybody listens, forget them all… | ||
emitter.clearListeners(["settle"]); | ||
emitter.removeAllSinks(); | ||
}, | ||
cancelable = (propagateResolve, propagateReject) => { | ||
const | ||
resolveAsap = ramda.thunkify(propagateResolve)(outcome), | ||
rejectAsap = ramda.thunkify(propagateReject)(outcome); | ||
switch (state) { | ||
case "pending": | ||
const | ||
unSubscribe = | ||
emitter.on("settle", ({ isSuccess, outcome: theOutcome }) => { | ||
(isSuccess ? propagateResolve : propagateReject)(theOutcome); | ||
unSubscribe(); | ||
}); | ||
sink = {resolve: propagateResolve, reject: propagateReject}, | ||
unsubscribe = () => { | ||
emitter.removeSink(sink); | ||
}; | ||
return unSubscribe; | ||
emitter.addSink(sink); | ||
return unsubscribe; | ||
case "cancelled": | ||
return noop$1; | ||
break; | ||
case "rejected": | ||
setTimeout(propagateReject, 0, outcome); | ||
return noop$1; | ||
queueMicrotask(rejectAsap); | ||
break; | ||
case "resolved": | ||
setTimeout(propagateResolve, 0, outcome); | ||
return noop$1; | ||
queueMicrotask(resolveAsap); | ||
break; | ||
default: | ||
setTimeout(propagateReject, 0, new Error(`Unexpected cancelable deferred state: "${state}"`)); | ||
return noop$1; | ||
queueMicrotask(() => { | ||
propagateReject(new Error(`Unexpected cancelable deferred state: "${state}"`)); | ||
}); | ||
} | ||
return noop$1; | ||
}; | ||
@@ -2044,3 +1592,2 @@ | ||
const | ||
emitter = new Emittery(), | ||
doNothing = () => undefined; | ||
@@ -2053,44 +1600,34 @@ | ||
isFinallyRejected = false, | ||
previousListenerCount = 0, | ||
finalOutcome; | ||
emitter.on(Emittery.listenerRemoved, (function() { | ||
return function onListenerRemoved() { | ||
//console.log(`listenerRemoved: count=${emitter.listenerCount("settle")}, previousCount=${previousListenerCount}`); | ||
if ((emitter.listenerCount("settle") === 0) && (previousListenerCount > 0) | ||
&& !isFinallyResolved && !isFinallyRejected) { | ||
const | ||
onLastSinkRemoved = () => { | ||
if (!isFinallyResolved && !isFinallyRejected) { | ||
// abort the running computation if the number of consumers drops to zero | ||
cancelRunningComputation(); | ||
} | ||
}, | ||
previousListenerCount = emitter.listenerCount("settle"); | ||
}; | ||
}())); | ||
// run once for all when the number of consumers exceeds zero | ||
onFirstSinkAdded = () => { | ||
cancelRunningComputation = cc( | ||
value => { | ||
finalOutcome = value; | ||
isFinallyResolved = true; | ||
emitter.emitOnce({outcome: value, isSuccess: true}); | ||
}, | ||
error => { | ||
finalOutcome = error; | ||
isFinallyRejected = true; | ||
emitter.emitOnce({outcome: error, isSuccess: false}); | ||
} | ||
); | ||
}, | ||
emitter.on(Emittery.listenerAdded, (function() { | ||
return function onListenerAdded() { | ||
//console.log(`listenerAdded: count=${emitter.listenerCount("settle")}, previousCount=${previousListenerCount}`); | ||
if ((emitter.listenerCount("settle") > 0) && (previousListenerCount === 0)) { | ||
// run once for all when the number of consumers exceeds zero | ||
cancelRunningComputation = cc( | ||
value => { | ||
finalOutcome = value; | ||
isFinallyResolved = true; | ||
emitter.emit("settle", {outcome: value, isSuccess: true}); | ||
}, | ||
error => { | ||
finalOutcome = error; | ||
isFinallyRejected = true; | ||
emitter.emit("settle", {outcome: error, isSuccess: false}); | ||
} | ||
); | ||
} | ||
emitter = emitterForSingleEmission({onFirstSinkAdded, onLastSinkRemoved}); | ||
previousListenerCount = emitter.listenerCount("settle"); | ||
}; | ||
}())); | ||
return (resolve, reject) => { | ||
if (isFinallyResolved) { | ||
setTimeout(resolve, 0, finalOutcome); | ||
const resolveAsap = ramda.thunkify(resolve)(finalOutcome); | ||
queueMicrotask(resolveAsap); | ||
return doNothing; | ||
@@ -2100,3 +1637,4 @@ } | ||
if (isFinallyRejected) { | ||
setTimeout(reject, 0, finalOutcome); | ||
const rejectAsap = ramda.thunkify(reject)(finalOutcome); | ||
queueMicrotask(rejectAsap); | ||
return doNothing; | ||
@@ -2106,8 +1644,7 @@ } | ||
const | ||
unConsume = | ||
emitter.on("settle", ({isSuccess, outcome}) => { | ||
(isSuccess ? resolve : reject)(outcome); | ||
unConsume(); | ||
}); | ||
sink = { resolve, reject }, | ||
unConsume = () => { emitter.removeSink(sink); }; | ||
emitter.addSink(sink); | ||
return unConsume; | ||
@@ -2114,0 +1651,0 @@ }; |
/* @license Apache-2.0 | ||
@visisoft/staticland v.2.0.0-alpha.3 visisoft.de | ||
(Build date: 10/24/2023 - 9:47:10 PM) | ||
@visisoft/staticland v.2.0.0 visisoft.de | ||
(Build date: 4/16/2024 - 6:25:26 PM) | ||
*/ | ||
@@ -963,539 +963,81 @@ 'use strict'; | ||
const anyMap = new WeakMap(); | ||
const eventsMap = new WeakMap(); | ||
const producersMap = new WeakMap(); | ||
/** | ||
* @template T | ||
* @typedef {{ resolve(t:T): void, reject(e: Error): void }} Sink | ||
*/ | ||
const anyProducer = Symbol('anyProducer'); | ||
const resolvedPromise = Promise.resolve(); | ||
/** | ||
* @template T | ||
* @typedef {{isSuccess: boolean, outcome: T|Error}} EmitterEvent | ||
*/ | ||
// Define symbols for "meta" events. | ||
const listenerAdded = Symbol('listenerAdded'); | ||
const listenerRemoved = Symbol('listenerRemoved'); | ||
/** | ||
* @template T | ||
* @typedef {{ emitOnce: (event: EmitterEvent<T>) => void, removeAllSinks: () => void, addSink: (sink: Sink<T>) => void, removeSink: (s: Sink<T>) => void }} Emitter | ||
*/ | ||
let canEmitMetaEvents = false; | ||
let isGlobalDebugEnabled = false; | ||
const | ||
/** | ||
* @template T | ||
* @param onLastSinkRemoved | ||
* @param onFirstSinkAdded | ||
* @return Emitter<T> | ||
*/ | ||
emitterForSingleEmission = ({ | ||
onLastSinkRemoved, | ||
onFirstSinkAdded | ||
}) => { | ||
const | ||
/** @type {Sink[]} */ | ||
sinks = [], | ||
function assertEventName(eventName) { | ||
if (typeof eventName !== 'string' && typeof eventName !== 'symbol' && typeof eventName !== 'number') { | ||
throw new TypeError('`eventName` must be a string, symbol, or number'); | ||
} | ||
} | ||
function assertListener(listener) { | ||
if (typeof listener !== 'function') { | ||
throw new TypeError('listener must be a function'); | ||
} | ||
} | ||
function getListeners(instance, eventName) { | ||
const events = eventsMap.get(instance); | ||
if (!events.has(eventName)) { | ||
return; | ||
} | ||
return events.get(eventName); | ||
} | ||
function getEventProducers(instance, eventName) { | ||
const key = typeof eventName === 'string' || typeof eventName === 'symbol' || typeof eventName === 'number' ? eventName : anyProducer; | ||
const producers = producersMap.get(instance); | ||
if (!producers.has(key)) { | ||
return; | ||
} | ||
return producers.get(key); | ||
} | ||
function enqueueProducers(instance, eventName, eventData) { | ||
const producers = producersMap.get(instance); | ||
if (producers.has(eventName)) { | ||
for (const producer of producers.get(eventName)) { | ||
producer.enqueue(eventData); | ||
} | ||
} | ||
if (producers.has(anyProducer)) { | ||
const item = Promise.all([eventName, eventData]); | ||
for (const producer of producers.get(anyProducer)) { | ||
producer.enqueue(item); | ||
} | ||
} | ||
} | ||
function iterator(instance, eventNames) { | ||
eventNames = Array.isArray(eventNames) ? eventNames : [eventNames]; | ||
let isFinished = false; | ||
let flush = () => {}; | ||
let queue = []; | ||
const producer = { | ||
enqueue(item) { | ||
queue.push(item); | ||
flush(); | ||
}, | ||
finish() { | ||
isFinished = true; | ||
flush(); | ||
}, | ||
}; | ||
for (const eventName of eventNames) { | ||
let set = getEventProducers(instance, eventName); | ||
if (!set) { | ||
set = new Set(); | ||
const producers = producersMap.get(instance); | ||
producers.set(eventName, set); | ||
} | ||
set.add(producer); | ||
} | ||
return { | ||
async next() { | ||
if (!queue) { | ||
return {done: true}; | ||
} | ||
if (queue.length === 0) { | ||
if (isFinished) { | ||
queue = undefined; | ||
return this.next(); | ||
/** | ||
* @template T | ||
* @param {Sink<T>} sink | ||
*/ | ||
addSink = sink => { | ||
sinks.push(sink); | ||
if (sinks.length === 1) { | ||
onFirstSinkAdded(); | ||
} | ||
}, | ||
await new Promise(resolve => { | ||
flush = resolve; | ||
}); | ||
return this.next(); | ||
} | ||
return { | ||
done: false, | ||
value: await queue.shift(), | ||
}; | ||
}, | ||
async return(value) { | ||
queue = undefined; | ||
for (const eventName of eventNames) { | ||
const set = getEventProducers(instance, eventName); | ||
if (set) { | ||
set.delete(producer); | ||
if (set.size === 0) { | ||
const producers = producersMap.get(instance); | ||
producers.delete(eventName); | ||
removeSink = sink => { | ||
const | ||
index = sinks.indexOf(sink); | ||
if (index !== -1) { | ||
sinks.splice(index, 1); | ||
if (sinks.length === 0) { | ||
onLastSinkRemoved(); | ||
} | ||
} | ||
} | ||
}, | ||
flush(); | ||
return arguments.length > 0 | ||
? {done: true, value: await value} | ||
: {done: true}; | ||
}, | ||
[Symbol.asyncIterator]() { | ||
return this; | ||
}, | ||
}; | ||
} | ||
function defaultMethodNamesOrAssert(methodNames) { | ||
if (methodNames === undefined) { | ||
return allEmitteryMethods; | ||
} | ||
if (!Array.isArray(methodNames)) { | ||
throw new TypeError('`methodNames` must be an array of strings'); | ||
} | ||
for (const methodName of methodNames) { | ||
if (!allEmitteryMethods.includes(methodName)) { | ||
if (typeof methodName !== 'string') { | ||
throw new TypeError('`methodNames` element must be a string'); | ||
} | ||
throw new Error(`${methodName} is not Emittery method`); | ||
} | ||
} | ||
return methodNames; | ||
} | ||
const isMetaEvent = eventName => eventName === listenerAdded || eventName === listenerRemoved; | ||
function emitMetaEvent(emitter, eventName, eventData) { | ||
if (isMetaEvent(eventName)) { | ||
try { | ||
canEmitMetaEvents = true; | ||
emitter.emit(eventName, eventData); | ||
} finally { | ||
canEmitMetaEvents = false; | ||
} | ||
} | ||
} | ||
class Emittery { | ||
static mixin(emitteryPropertyName, methodNames) { | ||
methodNames = defaultMethodNamesOrAssert(methodNames); | ||
return target => { | ||
if (typeof target !== 'function') { | ||
throw new TypeError('`target` must be function'); | ||
} | ||
for (const methodName of methodNames) { | ||
if (target.prototype[methodName] !== undefined) { | ||
throw new Error(`The property \`${methodName}\` already exists on \`target\``); | ||
removeAllSinks = () => { | ||
if (sinks.length > 0) { | ||
sinks.splice(0); | ||
onLastSinkRemoved(); | ||
} | ||
} | ||
}, | ||
function getEmitteryProperty() { | ||
Object.defineProperty(this, emitteryPropertyName, { | ||
enumerable: false, | ||
value: new Emittery(), | ||
/** | ||
* @template T | ||
* @param {EmitterEvent<T>} event | ||
*/ | ||
emitOnce = event => { | ||
const {isSuccess, outcome} = event; | ||
sinks.forEach(({resolve, reject}) => { | ||
(isSuccess ? resolve : reject)(outcome); | ||
}); | ||
return this[emitteryPropertyName]; | ||
} | ||
Object.defineProperty(target.prototype, emitteryPropertyName, { | ||
enumerable: false, | ||
get: getEmitteryProperty, | ||
}); | ||
const emitteryMethodCaller = methodName => function (...args) { | ||
return this[emitteryPropertyName][methodName](...args); | ||
removeAllSinks(); | ||
}; | ||
for (const methodName of methodNames) { | ||
Object.defineProperty(target.prototype, methodName, { | ||
enumerable: false, | ||
value: emitteryMethodCaller(methodName), | ||
}); | ||
} | ||
return target; | ||
return { | ||
addSink, | ||
removeSink, | ||
removeAllSinks, | ||
emitOnce | ||
}; | ||
} | ||
}; | ||
static get isDebugEnabled() { | ||
// In a browser environment, `globalThis.process` can potentially reference a DOM Element with a `#process` ID, | ||
// so instead of just type checking `globalThis.process`, we need to make sure that `globalThis.process.env` exists. | ||
// eslint-disable-next-line n/prefer-global/process | ||
if (typeof globalThis.process?.env !== 'object') { | ||
return isGlobalDebugEnabled; | ||
} | ||
// eslint-disable-next-line n/prefer-global/process | ||
const {env} = globalThis.process ?? {env: {}}; | ||
return env.DEBUG === 'emittery' || env.DEBUG === '*' || isGlobalDebugEnabled; | ||
} | ||
static set isDebugEnabled(newValue) { | ||
isGlobalDebugEnabled = newValue; | ||
} | ||
constructor(options = {}) { | ||
anyMap.set(this, new Set()); | ||
eventsMap.set(this, new Map()); | ||
producersMap.set(this, new Map()); | ||
producersMap.get(this).set(anyProducer, new Set()); | ||
this.debug = options.debug ?? {}; | ||
if (this.debug.enabled === undefined) { | ||
this.debug.enabled = false; | ||
} | ||
if (!this.debug.logger) { | ||
this.debug.logger = (type, debugName, eventName, eventData) => { | ||
try { | ||
// TODO: Use https://github.com/sindresorhus/safe-stringify when the package is more mature. Just copy-paste the code. | ||
eventData = JSON.stringify(eventData); | ||
} catch { | ||
eventData = `Object with the following keys failed to stringify: ${Object.keys(eventData).join(',')}`; | ||
} | ||
if (typeof eventName === 'symbol' || typeof eventName === 'number') { | ||
eventName = eventName.toString(); | ||
} | ||
const currentTime = new Date(); | ||
const logTime = `${currentTime.getHours()}:${currentTime.getMinutes()}:${currentTime.getSeconds()}.${currentTime.getMilliseconds()}`; | ||
console.log(`[${logTime}][emittery:${type}][${debugName}] Event Name: ${eventName}\n\tdata: ${eventData}`); | ||
}; | ||
} | ||
} | ||
logIfDebugEnabled(type, eventName, eventData) { | ||
if (Emittery.isDebugEnabled || this.debug.enabled) { | ||
this.debug.logger(type, this.debug.name, eventName, eventData); | ||
} | ||
} | ||
on(eventNames, listener) { | ||
assertListener(listener); | ||
eventNames = Array.isArray(eventNames) ? eventNames : [eventNames]; | ||
for (const eventName of eventNames) { | ||
assertEventName(eventName); | ||
let set = getListeners(this, eventName); | ||
if (!set) { | ||
set = new Set(); | ||
const events = eventsMap.get(this); | ||
events.set(eventName, set); | ||
} | ||
set.add(listener); | ||
this.logIfDebugEnabled('subscribe', eventName, undefined); | ||
if (!isMetaEvent(eventName)) { | ||
emitMetaEvent(this, listenerAdded, {eventName, listener}); | ||
} | ||
} | ||
return this.off.bind(this, eventNames, listener); | ||
} | ||
off(eventNames, listener) { | ||
assertListener(listener); | ||
eventNames = Array.isArray(eventNames) ? eventNames : [eventNames]; | ||
for (const eventName of eventNames) { | ||
assertEventName(eventName); | ||
const set = getListeners(this, eventName); | ||
if (set) { | ||
set.delete(listener); | ||
if (set.size === 0) { | ||
const events = eventsMap.get(this); | ||
events.delete(eventName); | ||
} | ||
} | ||
this.logIfDebugEnabled('unsubscribe', eventName, undefined); | ||
if (!isMetaEvent(eventName)) { | ||
emitMetaEvent(this, listenerRemoved, {eventName, listener}); | ||
} | ||
} | ||
} | ||
once(eventNames) { | ||
let off_; | ||
const promise = new Promise(resolve => { | ||
off_ = this.on(eventNames, data => { | ||
off_(); | ||
resolve(data); | ||
}); | ||
}); | ||
promise.off = off_; | ||
return promise; | ||
} | ||
events(eventNames) { | ||
eventNames = Array.isArray(eventNames) ? eventNames : [eventNames]; | ||
for (const eventName of eventNames) { | ||
assertEventName(eventName); | ||
} | ||
return iterator(this, eventNames); | ||
} | ||
async emit(eventName, eventData) { | ||
assertEventName(eventName); | ||
if (isMetaEvent(eventName) && !canEmitMetaEvents) { | ||
throw new TypeError('`eventName` cannot be meta event `listenerAdded` or `listenerRemoved`'); | ||
} | ||
this.logIfDebugEnabled('emit', eventName, eventData); | ||
enqueueProducers(this, eventName, eventData); | ||
const listeners = getListeners(this, eventName) ?? new Set(); | ||
const anyListeners = anyMap.get(this); | ||
const staticListeners = [...listeners]; | ||
const staticAnyListeners = isMetaEvent(eventName) ? [] : [...anyListeners]; | ||
await resolvedPromise; | ||
await Promise.all([ | ||
...staticListeners.map(async listener => { | ||
if (listeners.has(listener)) { | ||
return listener(eventData); | ||
} | ||
}), | ||
...staticAnyListeners.map(async listener => { | ||
if (anyListeners.has(listener)) { | ||
return listener(eventName, eventData); | ||
} | ||
}), | ||
]); | ||
} | ||
async emitSerial(eventName, eventData) { | ||
assertEventName(eventName); | ||
if (isMetaEvent(eventName) && !canEmitMetaEvents) { | ||
throw new TypeError('`eventName` cannot be meta event `listenerAdded` or `listenerRemoved`'); | ||
} | ||
this.logIfDebugEnabled('emitSerial', eventName, eventData); | ||
const listeners = getListeners(this, eventName) ?? new Set(); | ||
const anyListeners = anyMap.get(this); | ||
const staticListeners = [...listeners]; | ||
const staticAnyListeners = [...anyListeners]; | ||
await resolvedPromise; | ||
/* eslint-disable no-await-in-loop */ | ||
for (const listener of staticListeners) { | ||
if (listeners.has(listener)) { | ||
await listener(eventData); | ||
} | ||
} | ||
for (const listener of staticAnyListeners) { | ||
if (anyListeners.has(listener)) { | ||
await listener(eventName, eventData); | ||
} | ||
} | ||
/* eslint-enable no-await-in-loop */ | ||
} | ||
onAny(listener) { | ||
assertListener(listener); | ||
this.logIfDebugEnabled('subscribeAny', undefined, undefined); | ||
anyMap.get(this).add(listener); | ||
emitMetaEvent(this, listenerAdded, {listener}); | ||
return this.offAny.bind(this, listener); | ||
} | ||
anyEvent() { | ||
return iterator(this); | ||
} | ||
offAny(listener) { | ||
assertListener(listener); | ||
this.logIfDebugEnabled('unsubscribeAny', undefined, undefined); | ||
emitMetaEvent(this, listenerRemoved, {listener}); | ||
anyMap.get(this).delete(listener); | ||
} | ||
clearListeners(eventNames) { | ||
eventNames = Array.isArray(eventNames) ? eventNames : [eventNames]; | ||
for (const eventName of eventNames) { | ||
this.logIfDebugEnabled('clear', eventName, undefined); | ||
if (typeof eventName === 'string' || typeof eventName === 'symbol' || typeof eventName === 'number') { | ||
const set = getListeners(this, eventName); | ||
if (set) { | ||
set.clear(); | ||
} | ||
const producers = getEventProducers(this, eventName); | ||
if (producers) { | ||
for (const producer of producers) { | ||
producer.finish(); | ||
} | ||
producers.clear(); | ||
} | ||
} else { | ||
anyMap.get(this).clear(); | ||
for (const [eventName, listeners] of eventsMap.get(this).entries()) { | ||
listeners.clear(); | ||
eventsMap.get(this).delete(eventName); | ||
} | ||
for (const [eventName, producers] of producersMap.get(this).entries()) { | ||
for (const producer of producers) { | ||
producer.finish(); | ||
} | ||
producers.clear(); | ||
producersMap.get(this).delete(eventName); | ||
} | ||
} | ||
} | ||
} | ||
listenerCount(eventNames) { | ||
eventNames = Array.isArray(eventNames) ? eventNames : [eventNames]; | ||
let count = 0; | ||
for (const eventName of eventNames) { | ||
if (typeof eventName === 'string') { | ||
count += anyMap.get(this).size | ||
+ (getListeners(this, eventName)?.size ?? 0) | ||
+ (getEventProducers(this, eventName)?.size ?? 0) | ||
+ (getEventProducers(this)?.size ?? 0); | ||
continue; | ||
} | ||
if (typeof eventName !== 'undefined') { | ||
assertEventName(eventName); | ||
} | ||
count += anyMap.get(this).size; | ||
for (const value of eventsMap.get(this).values()) { | ||
count += value.size; | ||
} | ||
for (const value of producersMap.get(this).values()) { | ||
count += value.size; | ||
} | ||
} | ||
return count; | ||
} | ||
bindMethods(target, methodNames) { | ||
if (typeof target !== 'object' || target === null) { | ||
throw new TypeError('`target` must be an object'); | ||
} | ||
methodNames = defaultMethodNamesOrAssert(methodNames); | ||
for (const methodName of methodNames) { | ||
if (target[methodName] !== undefined) { | ||
throw new Error(`The property \`${methodName}\` already exists on \`target\``); | ||
} | ||
Object.defineProperty(target, methodName, { | ||
enumerable: false, | ||
value: this[methodName].bind(this), | ||
}); | ||
} | ||
} | ||
} | ||
const allEmitteryMethods = Object.getOwnPropertyNames(Emittery.prototype).filter(v => v !== 'constructor'); | ||
Object.defineProperty(Emittery, 'listenerAdded', { | ||
value: listenerAdded, | ||
writable: false, | ||
enumerable: true, | ||
configurable: false, | ||
}); | ||
Object.defineProperty(Emittery, 'listenerRemoved', { | ||
value: listenerRemoved, | ||
writable: false, | ||
enumerable: true, | ||
configurable: false, | ||
}); | ||
var __promiseToCancelable = promise => (res, rej) => { | ||
@@ -1540,3 +1082,2 @@ let | ||
const | ||
emitter = new Emittery(), | ||
doNothing = () => undefined; | ||
@@ -1549,44 +1090,34 @@ | ||
isFinallyRejected = false, | ||
previousListenerCount = 0, | ||
finalOutcome; | ||
emitter.on(Emittery.listenerRemoved, (function() { | ||
return function onListenerRemoved() { | ||
//console.log(`listenerRemoved: count=${emitter.listenerCount("settle")}, previousCount=${previousListenerCount}`); | ||
if ((emitter.listenerCount("settle") === 0) && (previousListenerCount > 0) | ||
&& !isFinallyResolved && !isFinallyRejected) { | ||
const | ||
onLastSinkRemoved = () => { | ||
if (!isFinallyResolved && !isFinallyRejected) { | ||
// abort the running computation if the number of consumers drops to zero | ||
cancelRunningComputation(); | ||
} | ||
}, | ||
previousListenerCount = emitter.listenerCount("settle"); | ||
}; | ||
}())); | ||
// run once for all when the number of consumers exceeds zero | ||
onFirstSinkAdded = () => { | ||
cancelRunningComputation = cc( | ||
value => { | ||
finalOutcome = value; | ||
isFinallyResolved = true; | ||
emitter.emitOnce({outcome: value, isSuccess: true}); | ||
}, | ||
error => { | ||
finalOutcome = error; | ||
isFinallyRejected = true; | ||
emitter.emitOnce({outcome: error, isSuccess: false}); | ||
} | ||
); | ||
}, | ||
emitter.on(Emittery.listenerAdded, (function() { | ||
return function onListenerAdded() { | ||
//console.log(`listenerAdded: count=${emitter.listenerCount("settle")}, previousCount=${previousListenerCount}`); | ||
if ((emitter.listenerCount("settle") > 0) && (previousListenerCount === 0)) { | ||
// run once for all when the number of consumers exceeds zero | ||
cancelRunningComputation = cc( | ||
value => { | ||
finalOutcome = value; | ||
isFinallyResolved = true; | ||
emitter.emit("settle", {outcome: value, isSuccess: true}); | ||
}, | ||
error => { | ||
finalOutcome = error; | ||
isFinallyRejected = true; | ||
emitter.emit("settle", {outcome: error, isSuccess: false}); | ||
} | ||
); | ||
} | ||
emitter = emitterForSingleEmission({onFirstSinkAdded, onLastSinkRemoved}); | ||
previousListenerCount = emitter.listenerCount("settle"); | ||
}; | ||
}())); | ||
return (resolve, reject) => { | ||
if (isFinallyResolved) { | ||
setTimeout(resolve, 0, finalOutcome); | ||
const resolveAsap = ramda.thunkify(resolve)(finalOutcome); | ||
queueMicrotask(resolveAsap); | ||
return doNothing; | ||
@@ -1596,3 +1127,4 @@ } | ||
if (isFinallyRejected) { | ||
setTimeout(reject, 0, finalOutcome); | ||
const rejectAsap = ramda.thunkify(reject)(finalOutcome); | ||
queueMicrotask(rejectAsap); | ||
return doNothing; | ||
@@ -1602,8 +1134,7 @@ } | ||
const | ||
unConsume = | ||
emitter.on("settle", ({isSuccess, outcome}) => { | ||
(isSuccess ? resolve : reject)(outcome); | ||
unConsume(); | ||
}); | ||
sink = { resolve, reject }, | ||
unConsume = () => { emitter.removeSink(sink); }; | ||
emitter.addSink(sink); | ||
return unConsume; | ||
@@ -1610,0 +1141,0 @@ }; |
@@ -7,4 +7,3 @@ { | ||
"dependencies": { | ||
"emittery": "^1.0.1", | ||
"ramda": "^0.28.0" | ||
"ramda": "^0.29.1" | ||
}, | ||
@@ -17,8 +16,8 @@ "description": "StaticLand functions for Algebraic Data Types based on native JavaScript types", | ||
"@rollup/plugin-node-resolve": "^15.2.1", | ||
"@types/ramda": "^0.28.23", | ||
"@types/ramda": "^0.29.12", | ||
"chai": "^4.3.8", | ||
"es-module-shims": "^0.4.6", | ||
"hirestime": "^6.1.0", | ||
"istanbul-badges-readme": "^1.8.5", | ||
"mocha": "^10.2.0", | ||
"msw": "^2.0.0", | ||
"rollup": "^3.29.2" | ||
@@ -108,3 +107,3 @@ }, | ||
"type": "module", | ||
"version": "2.0.0-alpha.3" | ||
"version": "2.0.0" | ||
} |
[](https://www.npmjs.com/package/@visisoft/staticland)  | ||
[](http://reactivex.io) | ||
[@visisoft/staticland](https://semmel.github.io/StaticLand/) v{{ config.meta.version }} | ||
[@visisoft/staticland](https://semmel.github.io/StaticLand/) | ||
==================== | ||
@@ -11,3 +12,3 @@ Support programming in functional pipelines by exposing a familiar set of operations on asynchronous, optional and faulty data. | ||
----- | ||
For the motivation I'd like to refer to [James Sinclair's post on StaticLand][sinclair-static-land]. | ||
Article series on FP by [James Sinclair][sinclair-static-land] and [Tom Harding][tom-harding-series]. | ||
@@ -148,3 +149,4 @@ Data Types | ||
[crocks]: https://crocks.dev/docs/crocks/ | ||
[sinclair-static-land]: https://jrsinclair.com/articles/2020/whats-more-fantastic-than-fantasy-land-static-land | ||
[sinclair-static-land]: https://jrsinclair.com/web-development/ | ||
[adispring-comment]: https://github.com/ramda/ramda/issues/3264#issuecomment-1101877126 | ||
[tom-harding-series]: http://www.tomharding.me/fantasy-land |
@@ -8,3 +8,4 @@ /** | ||
import Emittery from "emittery"; | ||
import createEmitter from './internal/emitter.js'; | ||
import { thunkify } from "ramda"; | ||
@@ -23,3 +24,3 @@ const | ||
const | ||
emitter = new Emittery(), | ||
emitter = createEmitter({onLastSinkRemoved: noop, onFirstSinkAdded: noop}), | ||
@@ -30,3 +31,3 @@ resolve = value => { | ||
// in case anybody already listens, tell them… | ||
emitter.emit("settle", {outcome: value, isSuccess: true}); | ||
emitter.emitOnce({outcome: value, isSuccess: true}); | ||
}, | ||
@@ -38,3 +39,3 @@ | ||
// in case anybody already listens, tell them… | ||
emitter.emit("settle", {outcome: error, isSuccess: false}); | ||
emitter.emitOnce({outcome: error, isSuccess: false}); | ||
}, | ||
@@ -45,28 +46,34 @@ | ||
// in case anybody listens, forget them all… | ||
emitter.clearListeners(["settle"]); | ||
emitter.removeAllSinks(); | ||
}, | ||
cancelable = (propagateResolve, propagateReject) => { | ||
const | ||
resolveAsap = thunkify(propagateResolve)(outcome), | ||
rejectAsap = thunkify(propagateReject)(outcome) | ||
switch (state) { | ||
case "pending": | ||
const | ||
unSubscribe = | ||
emitter.on("settle", ({ isSuccess, outcome: theOutcome }) => { | ||
(isSuccess ? propagateResolve : propagateReject)(theOutcome); | ||
unSubscribe(); | ||
}); | ||
sink = {resolve: propagateResolve, reject: propagateReject}, | ||
unsubscribe = () => { | ||
emitter.removeSink(sink); | ||
}; | ||
return unSubscribe; | ||
emitter.addSink(sink); | ||
return unsubscribe; | ||
case "cancelled": | ||
return noop; | ||
break; | ||
case "rejected": | ||
setTimeout(propagateReject, 0, outcome); | ||
return noop; | ||
queueMicrotask(rejectAsap); | ||
break; | ||
case "resolved": | ||
setTimeout(propagateResolve, 0, outcome); | ||
return noop; | ||
queueMicrotask(resolveAsap); | ||
break; | ||
default: | ||
setTimeout(propagateReject, 0, new Error(`Unexpected cancelable deferred state: "${state}"`)); | ||
return noop; | ||
queueMicrotask(() => { | ||
propagateReject(new Error(`Unexpected cancelable deferred state: "${state}"`)); | ||
}); | ||
} | ||
return noop; | ||
}; | ||
@@ -73,0 +80,0 @@ |
@@ -1,2 +0,3 @@ | ||
import Emittery from "emittery"; | ||
import createEmitter from './internal/emitter.js'; | ||
import { thunkify } from "ramda"; | ||
@@ -6,3 +7,2 @@ const | ||
const | ||
emitter = new Emittery(), | ||
doNothing = () => undefined; | ||
@@ -15,44 +15,34 @@ | ||
isFinallyRejected = false, | ||
previousListenerCount = 0, | ||
finalOutcome; | ||
emitter.on(Emittery.listenerRemoved, (function() { | ||
return function onListenerRemoved() { | ||
//console.log(`listenerRemoved: count=${emitter.listenerCount("settle")}, previousCount=${previousListenerCount}`); | ||
if ((emitter.listenerCount("settle") === 0) && (previousListenerCount > 0) | ||
&& !isFinallyResolved && !isFinallyRejected) { | ||
const | ||
onLastSinkRemoved = () => { | ||
if (!isFinallyResolved && !isFinallyRejected) { | ||
// abort the running computation if the number of consumers drops to zero | ||
cancelRunningComputation(); | ||
} | ||
}, | ||
previousListenerCount = emitter.listenerCount("settle"); | ||
}; | ||
}())); | ||
// run once for all when the number of consumers exceeds zero | ||
onFirstSinkAdded = () => { | ||
cancelRunningComputation = cc( | ||
value => { | ||
finalOutcome = value; | ||
isFinallyResolved = true; | ||
emitter.emitOnce({outcome: value, isSuccess: true}); | ||
}, | ||
error => { | ||
finalOutcome = error; | ||
isFinallyRejected = true; | ||
emitter.emitOnce({outcome: error, isSuccess: false}); | ||
} | ||
); | ||
}, | ||
emitter.on(Emittery.listenerAdded, (function() { | ||
return function onListenerAdded() { | ||
//console.log(`listenerAdded: count=${emitter.listenerCount("settle")}, previousCount=${previousListenerCount}`); | ||
if ((emitter.listenerCount("settle") > 0) && (previousListenerCount === 0)) { | ||
// run once for all when the number of consumers exceeds zero | ||
cancelRunningComputation = cc( | ||
value => { | ||
finalOutcome = value; | ||
isFinallyResolved = true; | ||
emitter.emit("settle", {outcome: value, isSuccess: true}); | ||
}, | ||
error => { | ||
finalOutcome = error; | ||
isFinallyRejected = true; | ||
emitter.emit("settle", {outcome: error, isSuccess: false}); | ||
} | ||
); | ||
} | ||
emitter = createEmitter({onFirstSinkAdded, onLastSinkRemoved}); | ||
previousListenerCount = emitter.listenerCount("settle"); | ||
}; | ||
}())); | ||
return (resolve, reject) => { | ||
if (isFinallyResolved) { | ||
setTimeout(resolve, 0, finalOutcome); | ||
const resolveAsap = thunkify(resolve)(finalOutcome); | ||
queueMicrotask(resolveAsap); | ||
return doNothing; | ||
@@ -62,3 +52,4 @@ } | ||
if (isFinallyRejected) { | ||
setTimeout(reject, 0, finalOutcome); | ||
const rejectAsap = thunkify(reject)(finalOutcome); | ||
queueMicrotask(rejectAsap); | ||
return doNothing; | ||
@@ -68,8 +59,7 @@ } | ||
const | ||
unConsume = | ||
emitter.on("settle", ({isSuccess, outcome}) => { | ||
(isSuccess ? resolve : reject)(outcome); | ||
unConsume(); | ||
}); | ||
sink = { resolve, reject }, | ||
unConsume = () => { emitter.removeSink(sink); }; | ||
emitter.addSink(sink); | ||
return unConsume; | ||
@@ -76,0 +66,0 @@ }; |
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
3
-25%126
0.8%0
-100%151
1.34%269359
-11.85%7748
-11.76%+ Added
- Removed
- Removed
- Removed
Updated