From dcad7a49d0fa9d17f5e8c8e7677548be2967f364 Mon Sep 17 00:00:00 2001 From: Hayleigh Thompson Date: Sat, 27 Jan 2024 17:27:03 +0000 Subject: :truck: Move runtime module into internals/. --- gleam.toml | 1 - priv/bin/esbuild | Bin 0 -> 9756978 bytes src/lustre.gleam | 2 +- src/lustre/internals/runtime.gleam | 260 +++++++++++++++++++++++++++++++++++++ src/lustre/runtime.gleam | 260 ------------------------------------- src/lustre/server.gleam | 36 ++--- test/lustre_test.gleam | 2 +- 7 files changed, 280 insertions(+), 281 deletions(-) create mode 100755 priv/bin/esbuild create mode 100644 src/lustre/internals/runtime.gleam delete mode 100644 src/lustre/runtime.gleam diff --git a/gleam.toml b/gleam.toml index b12e429..a7082ed 100644 --- a/gleam.toml +++ b/gleam.toml @@ -13,7 +13,6 @@ gleam = ">= 0.33.0" internal_modules = [ "lustre/internals", "lustre/internals/*", - "lustre/runtime", "lustre/try", ] diff --git a/priv/bin/esbuild b/priv/bin/esbuild new file mode 100755 index 0000000..8c5cb93 Binary files /dev/null and b/priv/bin/esbuild differ diff --git a/src/lustre.gleam b/src/lustre.gleam index 77e0bb5..6c58c85 100644 --- a/src/lustre.gleam +++ b/src/lustre.gleam @@ -89,7 +89,7 @@ import gleam/otp/actor.{type StartError} import gleam/result import lustre/effect.{type Effect} import lustre/element.{type Element, type Patch} -import lustre/runtime +import lustre/internals/runtime // TYPES ----------------------------------------------------------------------- diff --git a/src/lustre/internals/runtime.gleam b/src/lustre/internals/runtime.gleam new file mode 100644 index 0000000..646baf4 --- /dev/null +++ b/src/lustre/internals/runtime.gleam @@ -0,0 +1,260 @@ +// IMPORTS --------------------------------------------------------------------- + +import gleam/dict.{type Dict} +import gleam/dynamic.{type Decoder, type Dynamic} +import gleam/erlang/process.{type Selector, type Subject} +import gleam/function.{identity} +import gleam/list +import gleam/json.{type Json} +import gleam/option.{Some} +import gleam/otp/actor.{type Next, type StartError, Spec} +import gleam/result +import lustre/effect.{type Effect} +import lustre/element.{type Element, type Patch} +import lustre/internals/patch.{Diff, Init} +import lustre/internals/vdom + +// TYPES ----------------------------------------------------------------------- + +/// +/// +type State(runtime, model, msg) { + State( + self: Subject(Action(runtime, msg)), + model: model, + update: fn(model, msg) -> #(model, Effect(msg)), + view: fn(model) -> Element(msg), + html: Element(msg), + renderers: Dict(Dynamic, fn(Patch(msg)) -> Nil), + handlers: Dict(String, fn(Dynamic) -> Result(msg, Nil)), + on_attribute_change: Dict(String, Decoder(msg)), + ) +} + +/// +/// +pub type Action(runtime, msg) { + AddRenderer(Dynamic, fn(Patch(msg)) -> Nil) + Attrs(List(#(String, Dynamic))) + Batch(List(msg), Effect(msg)) + Debug(DebugAction) + Dispatch(msg) + Emit(String, Json) + Event(String, Dynamic) + RemoveRenderer(Dynamic) + SetSelector(Selector(Action(runtime, msg))) + Shutdown +} + +pub type DebugAction { + Model(reply: fn(Dynamic) -> Nil) + View(reply: fn(Element(Dynamic)) -> Nil) +} + +// ACTOR ----------------------------------------------------------------------- + +@target(erlang) +/// +/// +pub fn start( + init: #(model, Effect(msg)), + update: fn(model, msg) -> #(model, Effect(msg)), + view: fn(model) -> Element(msg), + on_attribute_change: Dict(String, Decoder(msg)), +) -> Result(Subject(Action(runtime, msg)), StartError) { + let timeout = 1000 + let init = fn() { + let self = process.new_subject() + let html = view(init.0) + let handlers = vdom.handlers(html) + let state = + State( + self, + init.0, + update, + view, + html, + dict.new(), + handlers, + on_attribute_change, + ) + let selector = process.selecting(process.new_selector(), self, identity) + + run_effects(init.1, self) + actor.Ready(state, selector) + } + + actor.start_spec(Spec(init, timeout, loop)) +} + +@target(erlang) +fn loop( + message: Action(runtime, msg), + state: State(runtime, model, msg), +) -> Next(Action(runtime, msg), State(runtime, model, msg)) { + case message { + Attrs(attrs) -> { + list.filter_map(attrs, fn(attr) { + case dict.get(state.on_attribute_change, attr.0) { + Error(_) -> Error(Nil) + Ok(decoder) -> + decoder(attr.1) + |> result.replace_error(Nil) + } + }) + |> Batch(effect.none()) + |> loop(state) + } + + AddRenderer(id, renderer) -> { + let renderers = dict.insert(state.renderers, id, renderer) + let next = State(..state, renderers: renderers) + + renderer(Init(dict.keys(state.on_attribute_change), state.html)) + actor.continue(next) + } + + Batch([], _) -> actor.continue(state) + Batch([msg], other_effects) -> { + let #(model, effects) = state.update(state.model, msg) + let html = state.view(model) + let diff = patch.elements(state.html, html) + let next = + State(..state, model: model, html: html, handlers: diff.handlers) + + run_effects(effect.batch([effects, other_effects]), state.self) + + case patch.is_empty_element_diff(diff) { + True -> Nil + False -> run_renderers(state.renderers, Diff(diff)) + } + + actor.continue(next) + } + Batch([msg, ..rest], other_effects) -> { + let #(model, effects) = state.update(state.model, msg) + let html = state.view(model) + let diff = patch.elements(state.html, html) + let next = + State(..state, model: model, html: html, handlers: diff.handlers) + + loop(Batch(rest, effect.batch([effects, other_effects])), next) + } + + Debug(Model(reply)) -> { + reply(dynamic.from(state.model)) + actor.continue(state) + } + + Debug(View(reply)) -> { + reply(element.map(state.html, dynamic.from)) + actor.continue(state) + } + + Dispatch(msg) -> { + let #(model, effects) = state.update(state.model, msg) + let html = state.view(model) + let diff = patch.elements(state.html, html) + let next = + State(..state, model: model, html: html, handlers: diff.handlers) + + run_effects(effects, state.self) + + case patch.is_empty_element_diff(diff) { + True -> Nil + False -> run_renderers(state.renderers, Diff(diff)) + } + + actor.continue(next) + } + + Emit(name, event) -> { + let patch = patch.Emit(name, event) + + run_renderers(state.renderers, patch) + actor.continue(state) + } + + Event(name, event) -> { + case dict.get(state.handlers, name) { + Error(_) -> actor.continue(state) + Ok(handler) -> { + handler(event) + |> result.map(Dispatch) + |> result.map(actor.send(state.self, _)) + |> result.unwrap(Nil) + + actor.continue(state) + } + } + } + + RemoveRenderer(id) -> { + let renderers = dict.delete(state.renderers, id) + let next = State(..state, renderers: renderers) + + actor.continue(next) + } + + SetSelector(selector) -> actor.Continue(state, Some(selector)) + Shutdown -> actor.Stop(process.Killed) + } +} + +// UTILS ----------------------------------------------------------------------- + +@target(erlang) +fn run_renderers( + renderers: Dict(any, fn(Patch(msg)) -> Nil), + patch: Patch(msg), +) -> Nil { + use _, _, renderer <- dict.fold(renderers, Nil) + renderer(patch) +} + +@target(erlang) +fn run_effects(effects: Effect(msg), self: Subject(Action(runtime, msg))) -> Nil { + let dispatch = fn(msg) { actor.send(self, Dispatch(msg)) } + let emit = fn(name, event) { actor.send(self, Emit(name, event)) } + + effect.perform(effects, dispatch, emit) +} + +// Empty implementations of every function in this module are required because we +// need to be able to build the codebase *locally* with the JavaScript target to +// bundle the server component runtime. +// +// For *consumers* of Lustre this is not a problem, Gleam will see this module is +// never included in any path reachable from JavaScript but when we're *inside the +// package* Gleam has no idea that is the case. + +@target(javascript) +pub fn start( + init: #(model, Effect(msg)), + update: fn(model, msg) -> #(model, Effect(msg)), + view: fn(model) -> Element(msg), + on_attribute_change: Dict(String, Decoder(msg)), +) -> Result(Subject(Action(runtime, msg)), StartError) { + panic +} + +@target(javascript) +fn loop( + message: Action(runtime, msg), + state: State(runtime, model, msg), +) -> Next(Action(runtime, msg), State(runtime, model, msg)) { + panic +} + +@target(javascript) +fn run_renderers( + renderers: Dict(any, fn(Patch(msg)) -> Nil), + patch: Patch(msg), +) -> Nil { + panic +} + +@target(javascript) +fn run_effects(effects: Effect(msg), self: Subject(Action(runtime, msg))) -> Nil { + panic +} diff --git a/src/lustre/runtime.gleam b/src/lustre/runtime.gleam deleted file mode 100644 index 646baf4..0000000 --- a/src/lustre/runtime.gleam +++ /dev/null @@ -1,260 +0,0 @@ -// IMPORTS --------------------------------------------------------------------- - -import gleam/dict.{type Dict} -import gleam/dynamic.{type Decoder, type Dynamic} -import gleam/erlang/process.{type Selector, type Subject} -import gleam/function.{identity} -import gleam/list -import gleam/json.{type Json} -import gleam/option.{Some} -import gleam/otp/actor.{type Next, type StartError, Spec} -import gleam/result -import lustre/effect.{type Effect} -import lustre/element.{type Element, type Patch} -import lustre/internals/patch.{Diff, Init} -import lustre/internals/vdom - -// TYPES ----------------------------------------------------------------------- - -/// -/// -type State(runtime, model, msg) { - State( - self: Subject(Action(runtime, msg)), - model: model, - update: fn(model, msg) -> #(model, Effect(msg)), - view: fn(model) -> Element(msg), - html: Element(msg), - renderers: Dict(Dynamic, fn(Patch(msg)) -> Nil), - handlers: Dict(String, fn(Dynamic) -> Result(msg, Nil)), - on_attribute_change: Dict(String, Decoder(msg)), - ) -} - -/// -/// -pub type Action(runtime, msg) { - AddRenderer(Dynamic, fn(Patch(msg)) -> Nil) - Attrs(List(#(String, Dynamic))) - Batch(List(msg), Effect(msg)) - Debug(DebugAction) - Dispatch(msg) - Emit(String, Json) - Event(String, Dynamic) - RemoveRenderer(Dynamic) - SetSelector(Selector(Action(runtime, msg))) - Shutdown -} - -pub type DebugAction { - Model(reply: fn(Dynamic) -> Nil) - View(reply: fn(Element(Dynamic)) -> Nil) -} - -// ACTOR ----------------------------------------------------------------------- - -@target(erlang) -/// -/// -pub fn start( - init: #(model, Effect(msg)), - update: fn(model, msg) -> #(model, Effect(msg)), - view: fn(model) -> Element(msg), - on_attribute_change: Dict(String, Decoder(msg)), -) -> Result(Subject(Action(runtime, msg)), StartError) { - let timeout = 1000 - let init = fn() { - let self = process.new_subject() - let html = view(init.0) - let handlers = vdom.handlers(html) - let state = - State( - self, - init.0, - update, - view, - html, - dict.new(), - handlers, - on_attribute_change, - ) - let selector = process.selecting(process.new_selector(), self, identity) - - run_effects(init.1, self) - actor.Ready(state, selector) - } - - actor.start_spec(Spec(init, timeout, loop)) -} - -@target(erlang) -fn loop( - message: Action(runtime, msg), - state: State(runtime, model, msg), -) -> Next(Action(runtime, msg), State(runtime, model, msg)) { - case message { - Attrs(attrs) -> { - list.filter_map(attrs, fn(attr) { - case dict.get(state.on_attribute_change, attr.0) { - Error(_) -> Error(Nil) - Ok(decoder) -> - decoder(attr.1) - |> result.replace_error(Nil) - } - }) - |> Batch(effect.none()) - |> loop(state) - } - - AddRenderer(id, renderer) -> { - let renderers = dict.insert(state.renderers, id, renderer) - let next = State(..state, renderers: renderers) - - renderer(Init(dict.keys(state.on_attribute_change), state.html)) - actor.continue(next) - } - - Batch([], _) -> actor.continue(state) - Batch([msg], other_effects) -> { - let #(model, effects) = state.update(state.model, msg) - let html = state.view(model) - let diff = patch.elements(state.html, html) - let next = - State(..state, model: model, html: html, handlers: diff.handlers) - - run_effects(effect.batch([effects, other_effects]), state.self) - - case patch.is_empty_element_diff(diff) { - True -> Nil - False -> run_renderers(state.renderers, Diff(diff)) - } - - actor.continue(next) - } - Batch([msg, ..rest], other_effects) -> { - let #(model, effects) = state.update(state.model, msg) - let html = state.view(model) - let diff = patch.elements(state.html, html) - let next = - State(..state, model: model, html: html, handlers: diff.handlers) - - loop(Batch(rest, effect.batch([effects, other_effects])), next) - } - - Debug(Model(reply)) -> { - reply(dynamic.from(state.model)) - actor.continue(state) - } - - Debug(View(reply)) -> { - reply(element.map(state.html, dynamic.from)) - actor.continue(state) - } - - Dispatch(msg) -> { - let #(model, effects) = state.update(state.model, msg) - let html = state.view(model) - let diff = patch.elements(state.html, html) - let next = - State(..state, model: model, html: html, handlers: diff.handlers) - - run_effects(effects, state.self) - - case patch.is_empty_element_diff(diff) { - True -> Nil - False -> run_renderers(state.renderers, Diff(diff)) - } - - actor.continue(next) - } - - Emit(name, event) -> { - let patch = patch.Emit(name, event) - - run_renderers(state.renderers, patch) - actor.continue(state) - } - - Event(name, event) -> { - case dict.get(state.handlers, name) { - Error(_) -> actor.continue(state) - Ok(handler) -> { - handler(event) - |> result.map(Dispatch) - |> result.map(actor.send(state.self, _)) - |> result.unwrap(Nil) - - actor.continue(state) - } - } - } - - RemoveRenderer(id) -> { - let renderers = dict.delete(state.renderers, id) - let next = State(..state, renderers: renderers) - - actor.continue(next) - } - - SetSelector(selector) -> actor.Continue(state, Some(selector)) - Shutdown -> actor.Stop(process.Killed) - } -} - -// UTILS ----------------------------------------------------------------------- - -@target(erlang) -fn run_renderers( - renderers: Dict(any, fn(Patch(msg)) -> Nil), - patch: Patch(msg), -) -> Nil { - use _, _, renderer <- dict.fold(renderers, Nil) - renderer(patch) -} - -@target(erlang) -fn run_effects(effects: Effect(msg), self: Subject(Action(runtime, msg))) -> Nil { - let dispatch = fn(msg) { actor.send(self, Dispatch(msg)) } - let emit = fn(name, event) { actor.send(self, Emit(name, event)) } - - effect.perform(effects, dispatch, emit) -} - -// Empty implementations of every function in this module are required because we -// need to be able to build the codebase *locally* with the JavaScript target to -// bundle the server component runtime. -// -// For *consumers* of Lustre this is not a problem, Gleam will see this module is -// never included in any path reachable from JavaScript but when we're *inside the -// package* Gleam has no idea that is the case. - -@target(javascript) -pub fn start( - init: #(model, Effect(msg)), - update: fn(model, msg) -> #(model, Effect(msg)), - view: fn(model) -> Element(msg), - on_attribute_change: Dict(String, Decoder(msg)), -) -> Result(Subject(Action(runtime, msg)), StartError) { - panic -} - -@target(javascript) -fn loop( - message: Action(runtime, msg), - state: State(runtime, model, msg), -) -> Next(Action(runtime, msg), State(runtime, model, msg)) { - panic -} - -@target(javascript) -fn run_renderers( - renderers: Dict(any, fn(Patch(msg)) -> Nil), - patch: Patch(msg), -) -> Nil { - panic -} - -@target(javascript) -fn run_effects(effects: Effect(msg), self: Subject(Action(runtime, msg))) -> Nil { - panic -} diff --git a/src/lustre/server.gleam b/src/lustre/server.gleam index cf6ebe6..1f7fef6 100644 --- a/src/lustre/server.gleam +++ b/src/lustre/server.gleam @@ -10,12 +10,12 @@ import lustre/attribute.{type Attribute, attribute} import lustre/effect.{type Effect} import lustre/element.{type Element, element} import lustre/internals/constants -import lustre/runtime.{type Action, Attrs, Event, SetSelector} +import lustre/internals/runtime.{type Action, Attrs, Event, SetSelector} // ELEMENTS -------------------------------------------------------------------- -/// A simple wrapper to render a `` element. -/// +/// A simple wrapper to render a `` element. +/// pub fn component(attrs: List(Attribute(msg))) -> Element(msg) { element("lustre-server-component", attrs, []) } @@ -24,9 +24,9 @@ pub fn component(attrs: List(Attribute(msg))) -> Element(msg) { /// The `route` attribute should always be included on a [`component`](#component) /// to tell the client runtime what path to initiate the WebSocket connection on. -/// -/// -/// +/// +/// +/// pub fn route(path: String) -> Attribute(msg) { attribute("route", path) } @@ -34,25 +34,25 @@ pub fn route(path: String) -> Attribute(msg) { /// Ocassionally you may want to attach custom data to an event sent to the server. /// This could be used to include a hash of the current build to detect if the /// event was sent from a stale client. -/// +/// /// ```gleam -/// +/// /// ``` -/// +/// pub fn data(json: Json) -> Attribute(msg) { json |> json.to_string |> attribute("data-lustre-data", _) } -/// Properties of the JavaScript event object are typically not serialisable. +/// Properties of the JavaScript event object are typically not serialisable. /// This means if we want to pass them to the server we need to copy them into /// a new object first. -/// +/// /// This attribute tells Lustre what properties to include. Properties can come /// from nested objects by using dot notation. For example, you could include the /// `id` of the target `element` by passing `["target.id"]`. -/// +/// /// ```gleam /// import gleam/dynamic /// import gleam/result.{try} @@ -60,21 +60,21 @@ pub fn data(json: Json) -> Attribute(msg) { /// import lustre/element/html /// import lustre/event /// import lustre/server -/// +/// /// pub fn custom_button(on_click: fn(String) -> msg) -> Element(msg) { /// let handler = fn(event) { /// use target <- try(dynamic.field("target", dynamic.dynamic)(event)) /// use id <- try(dynamic.field("id", dynamic.string)(target)) -/// +/// /// Ok(on_click(id)) /// } -/// +/// /// html.button([event.on_click(handler), server.include(["target.id"])], [ /// element.text("Click me!") /// ]) /// } /// ``` -/// +/// pub fn include(properties: List(String)) -> Attribute(msg) { properties |> json.array(json.string) @@ -85,13 +85,13 @@ pub fn include(properties: List(String)) -> Attribute(msg) { // EFFECTS --------------------------------------------------------------------- /// -/// +/// pub fn emit(event: String, data: Json) -> Effect(msg) { effect.event(event, data) } /// -/// +/// pub fn selector(sel: Selector(Action(runtime, msg))) -> Effect(msg) { do_selector(sel) } diff --git a/test/lustre_test.gleam b/test/lustre_test.gleam index c90dc8b..bae6c0f 100644 --- a/test/lustre_test.gleam +++ b/test/lustre_test.gleam @@ -10,7 +10,7 @@ import gleeunit import lustre import lustre/element import lustre/internals/patch -import lustre/runtime.{Debug, Dispatch, Shutdown, View} +import lustre/internals/runtime.{Debug, Dispatch, Shutdown, View} // MAIN ------------------------------------------------------------------------ -- cgit v1.2.3