UNPKG

@otpjs/gen_server

Version:

A gen_server implementation for @otpjs/core

448 lines (410 loc) 14.5 kB
import * as core from '@otpjs/core'; import { OTPError, Pid, Ref, t, l, cons } from '@otpjs/types'; import * as matching from '@otpjs/matching'; import * as gen from '@otpjs/gen'; import * as proc_lib from '@otpjs/proc_lib'; import * as Symbols from './symbols.js'; export { Symbols }; function log(ctx, ...args) { const logger = ctx.log.extend('gen_server'); return logger(...args); } const { ok, error, EXIT, normal } = core.Symbols; const { link, nolink, $gen_cast, $gen_call } = gen.Symbols; const { _ } = matching.Symbols; export async function start(ctx, name, callbacks, args = t()) { if (!t.isTuple(name) && name !== undefined) { args = callbacks || args; callbacks = name; name = undefined; } return gen.start(ctx, nolink, name, initializer(callbacks, args)); } export async function start_link(ctx, name, callbacks, args = t()) { if (!t.isTuple(name) && name !== undefined) { args = callbacks || args; callbacks = name; name = undefined; } return gen.start(ctx, link, name, initializer(callbacks, args)); } export const startLink = start_link; export async function call(ctx, pid, message, timeout = 5000) { return gen.call(ctx, pid, message, timeout); } export async function cast(ctx, pid, message) { gen.cast(ctx, pid, message); return ok; } export async function reply(ctx, to, response) { return gen.reply(ctx, to, response); } function initializer(callbacks, args) { const decision = matching.buildCase((is) => { is(t(ok, _), success); is(t(Symbols.stop, _), stop); is(_, invalid_init_response); function success(ctx, caller, response) { const [, initialState] = response; const state = initialState; proc_lib.initAck(ctx, caller, t(ok, ctx.self())); return enter_loop(ctx, callbacks, state); } function stop(ctx, caller, response) { const [_stop, reason] = response; log(ctx, 'initialize(stop: %o)', reason); proc_lib.initAck(ctx, caller, t(error, reason)); ctx.die(reason); } function invalid_init_response(ctx, caller, response) { log(ctx, 'initialize(stop: invalid_init_response)'); throw new OTPError('invalid_init_response'); } }); return async function initialize(ctx, caller) { try { log(ctx, 'initialize(args: %o)', args); const response = await callbacks.init(ctx, ...args); const next = decision.for(response); return next(ctx, caller, response); } catch (err) { log(ctx, 'initialize(error: %o)', err); proc_lib.initAck(ctx, caller, t(error, err)); throw err; } }; } export async function enter_loop(ctx, callbacks, state) { let timeout = Infinity; log(ctx, 'enterLoop(callbacks: %o)', callbacks); try { while (true) { log(ctx, 'enterLoop() : await receive()'); const message = await ctx.receiveBlock((given, after) => { given(_).then((message) => message); after(timeout).then(() => core.Symbols.timeout); }); log(ctx, 'enterLoop() : await receive() -> %o', message); const response = await loop(ctx, callbacks, message, state); log(ctx, 'enterLoop() : await loop() -> %o', response); const [, nextState, nextTimeout] = response; state = nextState; timeout = nextTimeout; } } catch (err) { log(ctx, 'enterLoop(error : %o)', err); return ctx.die(err); } } export const enterLoop = enter_loop; const loop = matching.clauses(function loop(route) { route(_, t($gen_call, t(Pid.isPid, Ref.isRef), _), _).to(call); route(_, t($gen_cast, _), _).to(cast); route(_, _, _).to(info); async function call(ctx, callbacks, incoming, state) { const [, from, call] = incoming; const result = await tryHandleCall(ctx, callbacks, call, from, state); return handleCallReply(ctx, callbacks, from, state, result); } async function cast(ctx, callbacks, incoming, state) { const [, cast] = incoming; const result = await tryDispatch( ctx, callbacks.handleCast, cast, state ); return handleCommonReply(ctx, callbacks, result, state); } async function info(ctx, callbacks, incoming, state) { const info = incoming; const result = await tryDispatch( ctx, callbacks.handleInfo, info, state ); return handleCommonReply(ctx, callbacks, result, state); } }); const replyWithNoTimeout = t(Symbols.reply, _, _); const replyWithTimeout = t(Symbols.reply, _, _, Number.isInteger); const noreplyWithNoTimeout = t(Symbols.noreply, _); const noreplyWithTimeout = t(Symbols.noreply, _, Number.isInteger); const stopNoReplyDemand = t(Symbols.stop, _, _); const stopReplyDemand = t(Symbols.stop, _, _, _); async function handleCallReply(ctx, callbacks, from, state, result) { const decision = matching.buildCase((is) => { is(t(ok, replyWithNoTimeout), ([ok, [, message, nextState]]) => { log(ctx, 'handleCallReply(from: %o, message: %o)', from, message); reply(ctx, from, message); return t(ok, nextState, Infinity); }); is(t(ok, replyWithTimeout), ([ok, [, message, nextState, timeout]]) => { reply(ctx, from, message); return t(ok, nextState, timeout); }); is(t(ok, noreplyWithNoTimeout), ([ok, [, nextState]]) => { return t(ok, nextState, Infinity); }); is(t(ok, noreplyWithTimeout), ([ok, [, nextState, timeout]]) => { return t(ok, nextState, timeout); }); is( t(ok, stopReplyDemand), async ([ok, [_stop, reason, response, nextState]]) => { try { return await terminate( ctx, callbacks, EXIT, reason, nextState, Error().stack ); } catch (err) { log( ctx, 'handleCallReply(response: %o, error: %o)', response, err ); reply(ctx, from, response); throw err; } } ); is( t(ok, stopNoReplyDemand), async ([ok, [_stop, reason, nextState]]) => { try { return await terminate( ctx, callbacks, EXIT, reason, nextState, Error().stack ); } catch (err) { log(ctx, 'handleCallReply(error: %o)', callbacks, err); throw err; } } ); is(_, (result) => { return handleCommonReply(ctx, callbacks, result, state); }); }); return decision.with(result); } const stopPattern = t(Symbols.stop, _, _); const exitPattern = t(EXIT, _, _, _); async function handleCommonReply(ctx, callbacks, result, state) { const decision = matching.buildCase((is) => { is(t(ok, stopPattern), async ([ok, [_stop, reason, state]]) => { return await terminate(ctx, callbacks, EXIT, reason, state); }); is(t(ok, noreplyWithNoTimeout), async ([ok, [_noreply, nextState]]) => t(ok, nextState, Infinity) ); is( t(ok, noreplyWithTimeout), async ([ok, [_noreply, nextState, timeout]]) => { log(ctx, 'handleCommonReply() : timeout : %o', timeout); return t(ok, nextState, timeout); } ); is(exitPattern, async ([_exit, type, reason, stack]) => { log(ctx, 'handleCommonReply(exit: %s)', stack); return await terminate(ctx, callbacks, type, reason, state, stack); }); is(t(ok, _), async ([, badReply]) => { log(ctx, 'handleCommonReply(badReply: %o)', badReply); return await terminate( ctx, callbacks, EXIT, t('bad_return_value', badReply), state, Error().stack ); }); /* istanbul ignore next */ is(_, async (result) => { log(ctx, 'handleCommonReply(badResult: %o)', result); return await terminate( ctx, callbacks, EXIT, t('bad_result_value', result), state, Error().stack ); }); }); return decision.with(result); } async function tryHandleCall(ctx, callbacks, message, from, state) { try { return t(ok, await callbacks.handleCall(ctx, message, from, state)); } catch (err) { if (err instanceof OTPError) { return t(EXIT, err.name, err.term, err.stack); } else if (err instanceof Error) { log(ctx, 'tryHandleCall(error: %o)', err.message); return t(EXIT, err.name, err.message, err.stack); } else { /* istanbul ignore next */ return t(ok, err); } } } async function tryDispatch(ctx, callback, message, state) { try { return t(ok, await callback(ctx, message, state)); } catch (err) { if (err instanceof OTPError) { return t(EXIT, err.name, err.term, err.stack); } else if (err instanceof Error) { log(ctx, 'tryDispatch(message: %o, error: %o)', message, err); return t(EXIT, err.name, err.message, err.stack); } else { /* istanbul ignore next */ return t(ok, err); } } } async function terminate(ctx, callbacks, type, reason, state, stack = null) { const response = await tryTerminate(ctx, callbacks, reason, state); log(ctx, 'terminate(reason: %o, response: %o)', reason, response); const decision = matching.buildCase((is) => { is(exitPattern, ([, , innerReason]) => { log( ctx, 'terminate(reason: %o) : throw OTPError(innerReason: %o)', reason, innerReason ); throw new OTPError(innerReason); }); is(_, () => { if (reason === normal) { throw normal; } else { log( ctx, 'terminate(reason: %o) : throw OTPError(reason: %o)', reason, reason ); throw new OTPError(reason); } }); }); return decision.with(response); } async function tryTerminate(ctx, callbacks, reason, state) { try { if (callbacks.terminate) { log( ctx, 'tryTerminate(callbacks.terminate: %o, reason: %o)', callbacks.terminate, reason ); await callbacks.terminate(ctx, reason, state); return ok; } else { log( ctx, 'tryTerminate(reason: %o) : terminate not implemented', reason ); return ok; } } catch (err) { if (err instanceof OTPError) { return t(EXIT, err.name, err.term, err.stack); } else if (err instanceof Error) { log(ctx, 'tryTerminate(reason: %o, error: %o)', reason, err); return t(EXIT, err.name, err.message, err.stack); } else { /* istanbul ignore next */ return err; } } } export function callbacks(builder) { let callHandlers = l(); let castHandlers = l(); let infoHandlers = l(); let init = null; let terminate = null; builder({ onInit(handler) { init = handler; }, onCall(pattern, handler) { callHandlers = cons( t(matching.compile(pattern), handler), callHandlers ); }, onCast(pattern, handler) { castHandlers = cons( t(matching.compile(pattern), handler), castHandlers ); }, onInfo(pattern, handler) { infoHandlers = cons( t(matching.compile(pattern), handler), infoHandlers ); }, onTerminate(handler) { terminate = handler; }, }); callHandlers = callHandlers.reverse(); castHandlers = castHandlers.reverse(); infoHandlers = infoHandlers.reverse(); return { init, handleCall, handleCast, handleInfo, terminate, }; function handleCall(ctx, call, from, state) { const found = callHandlers.find(([pattern, _handler]) => pattern(call)); ctx.log('handleCall(call: %o, handler: %o)', call, found); if (found) { const [, handler] = found; return handler(ctx, call, from, state); } else { throw new OTPError(t('unhandled_call', call)); } } function handleCast(ctx, cast, state) { const found = castHandlers.find(([pattern, _handler]) => pattern(cast)); ctx.log('handleCast(cast: %o, found: %o)', cast, found); if (found) { const [, handler] = found; return handler(ctx, cast, state); } else { throw new OTPError(t('unhandled_cast', cast)); } } function handleInfo(ctx, info, state) { const found = infoHandlers.find(([pattern, _handler]) => pattern(info)); ctx.log('handleInfo(info: %o, found: %o)', info, found); if (found) { const [, handler] = found; return handler(ctx, info, state); } else { throw new OTPError(t('unhandled_info', info)); } } }