Skip to content

Commit

Permalink
feat: cleanup, docs, remove unused code
Browse files Browse the repository at this point in the history
  • Loading branch information
hugojosefson committed Jul 11, 2023
1 parent eead9bc commit 0e210bc
Show file tree
Hide file tree
Showing 8 changed files with 153 additions and 96 deletions.
51 changes: 3 additions & 48 deletions src/fn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,6 @@ import { Logger, logger } from "./log.ts";

const log0: Logger = logger(import.meta.url);

/**
* Checks if we are definitively allowed to access a given environment variable.
* @param variable The name of the environment variable.
* @returns Whether we are allowed to access the environment variable.
*/
export async function isAllowedEnv(variable: string): Promise<boolean> {
const query = { name: "env", variable } as const;
const response = await Deno.permissions.query(query);
return response.state === "granted";
}

/**
* Gets an environment variable, but only if getting it is allowed already.
* @param variable The name of the environment variable.
* @returns The value of the environment variable, or undefined if it is not
* allowed, or not set.
*/
export async function weakEnvGet(
variable: string,
): Promise<string | undefined> {
if (await isAllowedEnv(variable)) {
return Deno.env.get(variable);
}
return undefined;
}

/**
* Checks if two items are the same, and the same type.
* @param items.a The first item.
Expand Down Expand Up @@ -105,33 +79,14 @@ export function safely<T = void>(
}

/**
* Converts a WebSocket readyState number to a string.
* @param readyState
*/
export function webSocketReadyState(readyState?: number): string {
switch (readyState) {
case WebSocket.CONNECTING:
return "CONNECTING";
case WebSocket.OPEN:
return "OPEN";
case WebSocket.CLOSING:
return "CLOSING";
case WebSocket.CLOSED:
return "CLOSED";
default:
return `UNKNOWN(${s(readyState)})`;
}
}

/**
* Create an {@link AbortController} that will also abort when the given
* Create an {@link AbortController} that can also abort when the given
* {@link AbortSignal} aborts.
* @param signal Any {@link AbortSignal} to forward abort events from.
* @returns An {@link AbortController} that will also abort when the given
* {@link AbortSignal} aborts.
*/
export function orSignalController(signal?: AbortSignal): AbortController {
const controller = new AbortController();
export function createOrAbortController(signal?: AbortSignal): AbortController {
const controller: AbortController = new AbortController();
if (signal) {
signal.addEventListener("abort", () => {
controller.abort();
Expand Down
18 changes: 10 additions & 8 deletions src/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,15 @@ function countLabel(label: string): number {
* @returns A {@link Logger}.
*/
export function logger(labelOrMetaImportUrl: string): Logger {
const label = calculateLabel(labelOrMetaImportUrl);
return Object.assign(
debug(`pid(${Deno.pid})/${label}/${countLabel(label)}`),
{
sub(subLabel: string): Logger {
return logger(`${label}:${subLabel}`);
},
},
const label: string = calculateLabel(labelOrMetaImportUrl);

const debugLogger: Debug = debug(
`pid(${Deno.pid})/${label}/${countLabel(label)}`,
);
const hasSub: Pick<Logger, "sub"> = {
sub(subLabel: string): Logger {
return logger(`${label}:${subLabel}`);
},
};
return Object.assign(debugLogger, hasSub) as Logger;
}
23 changes: 14 additions & 9 deletions src/serve-web-socket.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { orSignalController } from "./fn.ts";
import { createOrAbortController } from "./fn.ts";

/** Will be called when a newly connected WebSocket emits the "open" event. */
export type ServeWebSocketHandler = (
Expand Down Expand Up @@ -32,16 +32,19 @@ export interface WebSocketSpecificOptions {
* @returns A response that indicates that the client should have upgraded to a WebSocket.
*/
export function expectOnlyWebSocketUpgrade(): Response {
return new Response("Expected upgrade to WebSocket, but client didn't.", {
status: 400,
});
return new Response(
"Expected upgrade to WebSocket, but client didn't request that.",
{
status: 400,
},
);
}

/** Default options for {@link serveWebSocket}. */
export const defaultOptions: Partial<
export const defaultServeWebSocketOptions: Partial<
ServeWebSocketOptions | ServeWebSocketTlsOptions
> = {
predicate: anyWebSocketUpgradeRequest,
predicate: isWebSocketUpgradeRequest,
handler: expectOnlyWebSocketUpgrade,
onListen: ({ hostname, port }) => {
console.error(`Listening on ${hostname}:${port}.`);
Expand Down Expand Up @@ -71,7 +74,7 @@ export type ServeWebSocketTlsOptions =
* Does not care about the method or URL of the request.
* @param request The request to check.
*/
export function anyWebSocketUpgradeRequest(request: Request): boolean {
export function isWebSocketUpgradeRequest(request: Request): boolean {
return (
(
request.headers.get("Upgrade") ??
Expand All @@ -97,14 +100,16 @@ export function serveWebSocket(
const effectiveOptions:
& Required<WebSocketSpecificOptions>
& (ServeWebSocketOptions | ServeWebSocketTlsOptions) = {
...defaultOptions,
...defaultServeWebSocketOptions,
...options,
} as
& Required<WebSocketSpecificOptions>
& (ServeWebSocketOptions | ServeWebSocketTlsOptions);

/** Forward any abort signal from the user-supplied options, and give us power to abort also. */
const abortController = orSignalController(effectiveOptions.signal);
const abortController: AbortController = createOrAbortController(
effectiveOptions.signal,
);
effectiveOptions.signal = abortController.signal;

/** Wrap the user-supplied handler with a predicate that determines whether to handle the request as a WebSocket. */
Expand Down
80 changes: 72 additions & 8 deletions src/state-machine.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { equals, s } from "./fn.ts";
import { s } from "./fn.ts";
import { PromiseQueue } from "./promise-queue.ts";
import { Logger, logger } from "./log.ts";

Expand All @@ -8,6 +8,9 @@ const INITIAL_STATE = `[*]`;
const DEFAULT_ARROW = `-->`;
const FINAL_STATE = `[*]`;

/**
* Symbol used to mark a transition as a goto transition.
*/
const GOTO_SYMBOL: unique symbol = Symbol("goto");

export interface TransitionDefinition<S> {
Expand All @@ -26,6 +29,7 @@ export type OnDisallowedTransition<S, E extends ErrorResponse> = (
from: S,
to: S,
) => E;

export type OnBeforeTransition<S> = (
transition: TransitionDefinition<S>,
createTransition: (to: S) => TransitionDefinition<S>,
Expand All @@ -41,6 +45,18 @@ export function defaultOnDisallowedTransition<S>(from: S, to: S): never {
);
}

function defaultOnBeforeTransition<S>(
transition: TransitionDefinition<S>,
): TransitionDefinition<S> {
return transition;
}

/**
* A state machine.
*
* @template S The type of the states.
* @template E The type of the error response from the onDisallowedTransition callback.
*/
export class StateMachine<
S,
E extends ErrorResponse = never,
Expand All @@ -51,9 +67,17 @@ export class StateMachine<
private readonly transitions: Map<S, Map<S, TransitionMeta<S>>> = new Map();
private readonly promiseQueue: PromiseQueue = new PromiseQueue();

/**
* Construct a new StateMachine.
*
* @param initialState The initial state.
* @param onBeforeTransition optional callback that is called before each transition.
* @param onDisallowedTransition optional callback that is called when a transition is not allowed.
* @param allowedTransitions list of allowed transitions.
*/
constructor(
initialState: S,
onBeforeTransition: OnBeforeTransition<S> = (transition) => transition,
onBeforeTransition: OnBeforeTransition<S> = defaultOnBeforeTransition<S>,
onDisallowedTransition: OnDisallowedTransition<S, E> =
defaultOnDisallowedTransition<S>,
allowedTransitions: TransitionDefinition<S>[] = [],
Expand Down Expand Up @@ -114,6 +138,7 @@ export class StateMachine<
/** run transition function */
const fn = transition.fn;
const result: void | Promise<void> = fn(transition);

/** if transition function returns a Promise, enqueue it */
if (result instanceof Promise) {
return this.promiseQueue.enqueue(async () => {
Expand Down Expand Up @@ -151,6 +176,7 @@ export class StateMachine<
return `${state}`.replace(/ /g, "_");
}

/** get all states, from the transitions */
let states: S[] = Array.from(
new Set([
...this.transitions.keys(),
Expand All @@ -161,6 +187,8 @@ export class StateMachine<
]),
]),
);

/** extract details about each transition, in a convenient format */
let transitions: TransitionDefinition<S>[] = [];
for (const [from, fromTo] of this.transitions.entries()) {
for (const to of fromTo.keys()) {
Expand All @@ -174,10 +202,13 @@ export class StateMachine<
}
}

/** assume current state is initial */
const initial: S = this._state;

/** extract final states */
let finalStates: S[] = states.filter((state: S) => this.isFinal(state));

/** remove final states, transitions to them */
/** if requested, remove final states, transitions to them */
if (!includeFinal) {
states = states.filter((state: S) => !finalStates.includes(state));

Expand All @@ -189,9 +220,13 @@ export class StateMachine<
}

/**
* Returns the arrow to use for the transition. Final states are marked with
* a dotted arrow.
* @param _from from state, unused for now
* Returns the arrow to use for the transition.
*
* - Final states and initial state, are marked with a dotted arrow.
* - Transitions with only a goto function, are marked with a semi-thick arrow.
* - Transitions with a non-goto transition function, are marked with a thick arrow.
* - All other transitions are marked with a thin arrow.
* @param from from state
* @param to to state
* @param fn transition function
*/
Expand Down Expand Up @@ -226,6 +261,10 @@ export class StateMachine<
return `-[${modifiers.join()}]->`;
}

/**
* Returns the name of the transition function, without the prefix "bound ".
* @param name
*/
function unbound(name = ""): string {
return name.replace(/^bound /g, "");
}
Expand All @@ -240,6 +279,18 @@ export class StateMachine<
return name.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase();
}

/**
* Returns the description of the transition, cleaned up:
*
* - Shorten "and" to ",".
* - Shorten "goto" to "→".
* - Remove the "to" state, if there is only one available transition after.
* - Remove trailing "→".
* - Trim.
*
* @param description original description
* @param to to state
*/
function cleanupDescription(description = "", to: S): string {
description = description.trim();
description = description.replace(/,? and /g, ", ");
Expand Down Expand Up @@ -288,12 +339,13 @@ export class StateMachine<

return "";
})();
if (s.length === 0 || equals({ a: s, b: to })) {
if (s === "" || s === `${to}`) {
return "";
}
return `: ${s}`;
}

/** calculate the maximum width of each column, for prettier output */
const stateMaxWidth = Math.max(
...states.map((s) => this.escapePlantUmlString(`${s}`).length),
);
Expand Down Expand Up @@ -416,7 +468,7 @@ export class StateMachine<
throw new Error(
`Expected at least one available transition from ${
s(this._state)
}, found none.`,
} to a non-final state, found none.`,
);
}
const nonFinalTransitions: S[] = availableTransitions.filter(
Expand All @@ -435,6 +487,13 @@ export class StateMachine<
this.transitionTo(transition);
}

/**
* Utility function to create a transition function that only immediately
* transitions to the given state.
*
* @param instanceGetter A function that returns the state machine instance.
* @param to The state to transition to.
*/
static gotoFn<S>(
instanceGetter: () => StateMachine<S>,
to: S,
Expand All @@ -456,6 +515,11 @@ export class StateMachine<
return either.includes(this._state);
}

/**
* Checks if you may transition to the given state.
* @param to The state to transition to.
* @param from The state to transition from. Defaults to current state.
*/
mayTransitionTo(to: S, from?: S): boolean {
return this.getAvailableTransitions(from).includes(to);
}
Expand Down
Loading

0 comments on commit 0e210bc

Please sign in to comment.