You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
async-timeout-js/semaphore.js

184 lines
7.2 KiB

function SemaphoreDisposed(message) { this.message = message || "Semaphore was disposed"; Object.freeze(this); }
// despite any similarity to the well-known and famous `is-promise` npm library, this is all my own code typed up completely so fuck your licenses.
const is_promise = obj => !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function';
/// An async semaphore with up to `cap` locks held at once.
///
/// To use as a mutex, make `cap` 1.
///
/// # Throws
/// If `cap` is 0 or lower.
function Semaphore(cap)
{
/*function Handle(next, id)
{
this.state = next;
this.id = id || new_id();
}
Handle.create = function(resolve, reject, id) {
let handle = new Handle(this, id);
handle.state[handle.id] = (error) => error ? reject(error) : resolve();
return handle;
};*/
if(!cap || cap <= 0) throw `Semaphore capacity cannot be '${cap}', must be a number greater than 0`;
this.capacity = cap;
this.length = 0;
let self = this;
// Waiters for the seamphore
var next = {
waiters: [],
// wait for a slot to be available, then take it. If there are slots, the promise resolves immediately, if not, it is added to the release queue and will resolve when there is one available for it.
acquire: () => {
return new Promise((resolve, reject) => { // next.waiters.push(error => error ? reject(error) : resolve()))
if(self.length < self.capacity) {
// Slot available, return now.
self.length += 1;
resolve(); //TODO: Add IDs? pass a Handle to resolve?
} else {
// Slot unavailable, add to queue.
next.waiters.push(resolve);//error => error ? reject(error) : resolve());
}
});
},
// release a raw lock on the semaphore. if there are pending `acquire()` promises, the length is not decremented, but the next queued up acquire is resolved insetad.
release: () => {
if(next.waiters.length>0) // There are pending waiters
next.waiters.shift()(); // No need to decrement, there is a pending waiter and that slot goes directly to them. (asyn)
else self.length -= 1; // Deremenet the length (imm)
}
//TODO: ,dispose: (error = new SemaphoreDisposed()) => { } // reject all `waiters`, and cause all new `acquire()`s to immediately reject.
};
this.acquire_raw = async () => {
await next.acquire();
};
/// Attempt to acquire the semaphore lock if there is one available.
///
/// # Returns
/// `true` if the lock was acquired, `false` if the lock must be waited on to be acquired.
this.try_acquire_raw = () => {
if(self.length < self.capacity) {
self.length+=1; return true;
} return false;
};
this.release_raw = () => {
next.release();
};
/// Acquire a semaphore lock (if one is not available, wait until one is) and then run `func()` with the lock held, afterwards, release the lock and return the result of `func()`.
/// If `func` is an async function, it is awaited and the lock is released after it has resolved (or rejected), and the result of this function is the result of that promise.
this.using = async (func) => {
//if(!func || (typeof obj !== "function" && !is_promise(func))) throw `Parameter '{func}' is not a function or Promise.`;
await self.acquire_raw();
try {
const rv = func();
if(is_promise(rv)) return await rv;
else return rv;
} finally {
self.release_raw();
}
};
/// Attempt to acquire a semaphore lock and execute `func()` with that lock held. If there is no available lock, immediately return and do not attempt to run `func()`.
///
/// # Async `func()`
/// If `func` is an async function, the return value of this function will be an awaitable promise that will release the lock upon awaiting (resolved or rejected), and yield the value of the awaited promise.
///
/// # Returns
/// You can use the second parameter to the function, the object `opt`, to choose how success/failure is represented in the returned value from the function call/returned promise (in the case of `func()` returning a promise (see above))
///
/// ## On successfully acquireing the lock:
/// * If `opt.wrap === true`: An object in the form of `{success: true, value: <returned value>}`. If `opt.keep_func === "always"`, the object will include the field `"func": func` as well. (See below)
/// * Otherwise, just the returned value itself. (*default*)
///
/// ## On failing to acquire the lock:
/// * If `opt.wrap === true`: An object in the form of `{success: false, value: opt.or, func: <see below>}`
/// * Otherwise, just `opt.or`. (*default*)
///
/// ### Notes
/// * If `opt` has no field called `or`, `undefined` is used instead.
/// * If `opt` has a field called `keep_func` and that field is `=== true` or `=== "always"`, *and* `opt.wrap === true`: On a failure to acquire the lock, the wrapped object will be in the form `{ value: (opt.or || undefined), success: false, "func": func }`.
this.try_using = (func, opt) => {
opt = opt || { wrap: false, or: undefined, keep_func: false };
const wrap_success = (value, success) => {
if(opt.wrap === true) return { "value": value,
"success": success,
"func": (((!success && opt.keep_func === true)
|| (opt.keep_func === "always"))
? func
: undefined) };
else return value;
};
if(self.try_acquire_raw()) {
const try_eval = () => {
try {
return func();
} catch(e) { self.release_raw(); throw(e); }
};
const rv = try_eval(); // If an exception is thrown, the lock is released and then the exception is propagated from here.
if(is_promise(rv)) return (async () => {
// `rv` is a promise, wrap it's awaiting inside a new promise that release the lock regardless of if awaiting `rv` throws.
try {
return wrap_success(await rv, true);
} /*catch(e) { // Prevent an exception from crashing, use it to resolve the promise instead. //XXX: This might not be needed, since we are returning this promise, it should be awaited which will prevent the exception from bubbling up.
return wrap_success(opt.or, false);
}*/ finally { self.release_raw(); }
})();
else {
// `rv` is not a promise, we can release the lock and return the value
self.release_raw();
return wrap_success(rv, true);
}
} else return wrap_success(opt.or, false);
};
/// Returns a lambda that, when executed, runs `using(func)` and returns the awaitable promise.
this.bind_using = (func) => { return () => self.using(func); };
}
/// Create a mutex (single cap semaphore)
Semaphore.mutex = () => new Semaphore(1);
const sem_example = async (locks) => {
let semaphore = new Semaphore(locks || 1); //Semaphore.Mutex();
const gen_runner = (name, num, time) => {
if(!time || time < 0) return semaphore.bind_using(() => {
for(let i=0;i<num;i++) console.log(`${name}: ${i}`);
return num;
});
else return semaphore.bind_using(async () => {
for(let i=0;i<num;i++) {
await new Promise(resolve => setTimeout(resolve, time));
console.log(`${name}: ${i}`);
}
return num;
});
};
return await Promise.all([
gen_runner("first", 10, 100)(),
gen_runner("second", 15, 75)(),
gen_runner("third", 500)(),
semaphore.try_using(() => {
for(let i=0;i<1000;i++) console.log(`!FOURTH: ${i}`);
return 1000;
}, {wrap: true, or: false, keep_func: true}),
]);
};
//sem_example();