aboutsummaryrefslogtreecommitdiff
path: root/src/server-runtime.ffi.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'src/server-runtime.ffi.mjs')
-rw-r--r--src/server-runtime.ffi.mjs143
1 files changed, 143 insertions, 0 deletions
diff --git a/src/server-runtime.ffi.mjs b/src/server-runtime.ffi.mjs
new file mode 100644
index 0000000..e82e82d
--- /dev/null
+++ b/src/server-runtime.ffi.mjs
@@ -0,0 +1,143 @@
+import { Ok, isEqual } from "./gleam.mjs";
+import {
+ AddRenderer,
+ Dispatch,
+ Event,
+ RemoveRenderer,
+ Shutdown,
+} from "./lustre/runtime.mjs";
+
+export class LustreServerApplication {
+ #queue = [];
+ #effects = [];
+ #didUpdate = false;
+
+ #vdom = null;
+ #handlers = new Map();
+ #renderers = new Set();
+
+ #model = null;
+ #update = null;
+ #view = null;
+
+ static start(flags, init, update, view) {
+ const app = new LustreServerApplication(init(flags), update, view, root);
+
+ return new Ok((msg) => app.send(msg));
+ }
+
+ // PUBLIC METHODS ------------------------------------------------------------
+
+ constructor([model, effects], update, view) {
+ this.#model = model;
+ this.#update = update;
+ this.#view = view;
+ this.#vdom = this.#view(this.#model);
+ this.#effects = effects.all.toArray();
+ this.#didUpdate = true;
+
+ globalThis.queueMicrotask(() => this.#tick());
+ }
+
+ send(action) {
+ switch (true) {
+ case action instanceof AddRenderer: {
+ this.#renderers.add(action[0]);
+ return;
+ }
+
+ case action instanceof Dispatch: {
+ this.#queue.push(action[0]);
+ this.#tick();
+
+ return;
+ }
+
+ case action instanceof Event: {
+ const [event, data] = action;
+
+ if (this.#handlers.has(event)) {
+ const msg = this.#handlers.get(event)(data);
+
+ if (msg.isOk()) {
+ this.#queue.push(msg[0]);
+ this.#tick();
+ }
+ }
+ }
+
+ case action instanceof RemoveRenderer: {
+ this.#renderers.delete(action[0]);
+ return;
+ }
+
+ case action instanceof Shutdown: {
+ this.#shutdown();
+ return;
+ }
+
+ default:
+ return;
+ }
+ }
+
+ // PRIVATE METHODS -----------------------------------------------------------
+
+ #tick() {
+ this.#flush_queue();
+
+ if (this.#didUpdate) {
+ this.#vdom = this.#view(this.#model);
+
+ for (const renderer of this.#renderers) {
+ renderer.render(this.#vdom);
+ }
+ }
+ }
+
+ #flush_queue(iterations = 0) {
+ while (this.#queue.length) {
+ const [next, effects] = this.#update(this.#model, this.#queue.shift());
+
+ this.#model = next;
+ this.#didUpdate ||= !isEqual(this.#model, next);
+ this.#effects = this.#effects.concat(effects.all.toArray());
+ }
+
+ while (this.#effects.length) {
+ this.#effects.shift()(
+ (msg) => this.send(new Dispatch(msg)),
+ (event, data) => this.emit(event, data)
+ );
+ }
+
+ if (this.#queue.length) {
+ if (iterations < 5) {
+ this.#flush_queue(++iterations);
+ } else {
+ window.requestAnimationFrame(() => this.#tick());
+ }
+ }
+ }
+
+ #shutdown() {
+ this.#model = null;
+ this.#queue = [];
+ this.#effects = [];
+ this.#didUpdate = false;
+ this.#update = () => {};
+ this.#view = () => {};
+ this.#vdom = null;
+ this.#handlers = new Map();
+ this.#renderers = new Set();
+ }
+}
+
+export const start = (app, selector, flags) =>
+ LustreClientApplication.start(
+ flags,
+ selector,
+ app.init,
+ app.update,
+ app.view
+ );