Skip to content

Commit

Permalink
Feat/basic state manager (#9)
Browse files Browse the repository at this point in the history
* FEAT/dispatcher added

* FEAT/dispatcher tests added

* FEAT/createApp fn added

* FEAT/exports app from index.ts
  • Loading branch information
AugustinSorel authored Dec 31, 2023
1 parent 9511b68 commit 3371499
Show file tree
Hide file tree
Showing 4 changed files with 190 additions and 0 deletions.
62 changes: 62 additions & 0 deletions packages/runtime/src/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { destroyDom } from "./destroy-dom";
import { Dispatcher } from "./dispatcher";
import { VNodes } from "./h";
import { mountDOM } from "./mount-dom";

type Props = {
state: any;
view: (state: any, emit: any) => VNodes;
reducers: Record<string, Function>;
};

export function createApp({ state, view, reducers }: Props) {
let parentEl: HTMLElement | null = null;
let vdom: VNodes | null = null;

const dispatcher = new Dispatcher();

const emit = (eventName: string, payload: any) => {
dispatcher.dispatch(eventName, payload);
};

const renderApp = () => {
if (vdom) {
destroyDom(vdom);
}

vdom = view(state, emit);

if (vdom && parentEl) {
mountDOM(vdom, parentEl);
}
};

const subscriptions = [dispatcher.afterEveryCommand(renderApp)];

for (const actionName in reducers) {
const reducer = reducers[actionName];
const subs = dispatcher.subscribe(actionName, (payload: any) => {
state = reducer(state, payload);
});
subscriptions.push(subs);
}

return {
mount: (_parentEl: HTMLElement) => {
parentEl = _parentEl;
renderApp();
},

unmount: () => {
if (vdom) {
destroyDom(vdom);
}

vdom = null;

for (const subscription of subscriptions) {
subscription();
}
},
};
}
52 changes: 52 additions & 0 deletions packages/runtime/src/dispatcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
export class Dispatcher {
private subs = new Map<string, Function[]>();
private afterHandlers: Function[] = [];

public subscribe = (commandName: string, handler: Function) => {
if (!this.subs.has(commandName)) {
this.subs.set(commandName, []);
}

const handlers = this.subs.get(commandName);

if (!handlers) {
throw new Error("handlers cannot be undefined");
}

if (handlers.includes(handler)) {
return () => {};
}

handlers.push(handler);

return () => {
const idx = handlers.indexOf(handler);
handlers.splice(idx, 1);
};
};

public afterEveryCommand = (handler: Function) => {
this.afterHandlers.push(handler);

return () => {
const idx = this.afterHandlers.indexOf(handler);
this.afterHandlers.splice(idx, 1);
};
};

public dispatch = (commandName: string, payload: any) => {
const handlers = this.subs.get(commandName);

if (handlers) {
for (const handler of handlers) {
handler(payload);
}
} else {
console.warn(`No handlers for command: ${commandName}`);
}

for (const afterHandler of this.afterHandlers) {
afterHandler();
}
};
}
1 change: 1 addition & 0 deletions packages/runtime/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./h";
export * from "./mount-dom";
export * from "./app";
75 changes: 75 additions & 0 deletions packages/runtime/tests/dispatcher.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { describe, expect, it, vi } from "vitest";
import { Dispatcher } from "../src/dispatcher";

const commandName = "test-event";
const payload = { test: "payload" };

describe("A command dispatcher", () => {
it("can register and unregister handlers to specific commands", () => {
const dispatcher = new Dispatcher();
const handler = vi.fn();

const unsubscribe = dispatcher.subscribe(commandName, handler);
dispatcher.dispatch(commandName, payload);

expect(handler).toHaveBeenCalledWith(payload);

unsubscribe();
dispatcher.dispatch(commandName, payload);

expect(handler).toHaveBeenCalledTimes(1);
});

it("can't register the same handler twice", () => {
const dispatcher = new Dispatcher();
const handler = vi.fn();

const unsubscribe = dispatcher.subscribe(commandName, handler);
dispatcher.subscribe(commandName, handler);
dispatcher.subscribe(commandName, handler);

dispatcher.dispatch(commandName, payload);

expect(handler).toHaveBeenCalledTimes(1);

unsubscribe();
dispatcher.dispatch(commandName, payload);

expect(handler).toHaveBeenCalledTimes(1);
});

it("unsubscribe multiple handlers", () => {
const dispatcher = new Dispatcher();
const handler1 = vi.fn();
const handler2 = vi.fn();

const unsubscribe1 = dispatcher.subscribe(commandName, handler1);
const unsubscribe2 = dispatcher.subscribe(commandName, handler2);
dispatcher.dispatch(commandName, payload);

expect(handler1).toHaveBeenCalledWith(payload);
expect(handler2).toHaveBeenCalledWith(payload);

unsubscribe1();
unsubscribe2();
dispatcher.dispatch(commandName, payload);

expect(handler1).toHaveBeenCalledTimes(1);
expect(handler2).toHaveBeenCalledTimes(1);
});

it("can register and unregister handlers that run after each command", () => {
const dispatcher = new Dispatcher();
const handler = vi.fn();

const unsubscribe = dispatcher.afterEveryCommand(handler);
dispatcher.dispatch(commandName, payload);

expect(handler).toHaveBeenCalled();

unsubscribe();
dispatcher.dispatch(commandName, payload);

expect(handler).toHaveBeenCalledTimes(1);
});
});

0 comments on commit 3371499

Please sign in to comment.