-module(agnostic@effect). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/agnostic/effect.gleam"). -export([none/0, from/1, deferred/2, event/2, select/1, provide/2, subscribe/2, unsubscribe/1, batch/1, map/2, perform/8]). -export_type([effect/1, actions/1]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. ?MODULEDOC( " In many frontend frameworks it's common for components to perform side effects\n" " whenever the need them. An event handler might make an HTTP request, or a\n" " component might reach into the DOM to focus an input.\n" "\n" " In Lustre we try to keep side effects separate from our main program loop.\n" " This comes with a whole bunch of benefits like making it easier to test and\n" " reason about our code, making it possible to implement time-travel debugging,\n" " or even to run our app on the server using Lustre's server components. This\n" " is great but we still need to perform side effects at some point, so how do\n" " we do that?\n" "\n" " The answer is through the `Effect` type that treats side effects as *data*.\n" " This approach is known as having **managed effects**: you pass data that\n" " describes a side effect to Lustre's runtime and it takes care of performing\n" " that effect and potentially sending messages back to your program for you.\n" " By going through this abstraction we discourage side effects from being\n" " performed in the middle of our program.\n" "\n" " ## Related packages\n" "\n" " While Lustre doesn't include many built-in effects, there are a number of\n" " community packages define useful common effects for your applications.\n" "\n" " - [`rsvp`](https://hexdocs.pm/rsvp) – Send HTTP requests from Lustre\n" " applications and server components.\n" "\n" " - [`modem`](https://hexdocs.pm/modem) – A friendly Lustre package to help\n" " you build a router, handle links, and manage URLs.\n" "\n" " - [`plinth`](https://hexdocs.pm/plinth) – Bindings to Node.js and browser\n" " platform APIs. (This package does not include any effects directly, but\n" " it does provide bindings to many APIs that you can use to create your\n" " own.)\n" "\n" " ## Effect timing\n" "\n" " Effects created with [`from`](#from) carry no timing guarantee beyond\n" " running during effect processing of the dispatch that produced them.\n" " Deferred effects — those tied to a point in a platform's render lifecycle —\n" " are provided by the platform modules instead of this one:\n" " [`dom.before_paint`](./platform/dom.html#before_paint) and\n" " [`dom.after_paint`](./platform/dom.html#after_paint) for the browser DOM,\n" " and [`opentui/effect.after_flush`](./platform/opentui/effect.html#after_flush)\n" " for OpenTUI. Platform authors can build their own deferred effects with\n" " [`deferred`](#deferred).\n" "\n" " ## Examples\n" "\n" " For folks coming from other languages (or other Gleam code!) where side\n" " effects are often performed in-place, this can feel a bit strange. We have\n" " a category of example apps dedicated to showing various effects in action:\n" "\n" " - [HTTP requests](https://github.com/lustre-labs/lustre/tree/main/examples/03-effects/01-http-requests)\n" "\n" " - [Generating random values](https://github.com/lustre-labs/lustre/tree/main/examples/03-effects/02-random)\n" "\n" " - [Setting up timers](https://github.com/lustre-labs/lustre/tree/main/examples/03-effects/03-timers)\n" "\n" " - [Working with LocalStorage](https://github.com/lustre-labs/lustre/tree/main/examples/03-effects/04-local-storage)\n" "\n" " - [Reading from the DOM](https://github.com/lustre-labs/lustre/tree/main/examples/03-effects/05-dom-effects)\n" "\n" " - [Optimistic state updates](https://github.com/lustre-labs/lustre/tree/main/examples/03-effects/06-optimistic-requests)\n" "\n" " This list of examples is likely to grow over time, so be sure to check back\n" " every now and then to see what's new!\n" "\n" " ## Getting help\n" "\n" " If you're having trouble with Lustre or not sure what the right way to do\n" " something is, the best place to get help is the [Gleam Discord server](https://discord.gg/Fm8Pwmy).\n" " You could also open an issue on the [Lustre GitHub repository](https://github.com/lustre-labs/lustre/issues).\n" "\n" ). -opaque effect(GIU) :: {effect, list(fun((actions(GIU)) -> nil)), list({binary(), fun((actions(GIU)) -> nil)})}. -type actions(GIV) :: {actions, fun((GIV) -> nil), fun((binary(), gleam@json:json()) -> nil), fun((gleam@erlang@process:selector(GIV)) -> nil), fun(() -> gleam@dynamic:dynamic_()), fun((binary(), gleam@json:json()) -> nil), fun((binary(), gleam@dynamic@decode:decoder(GIV)) -> nil), fun((binary()) -> nil)}. -file("src/agnostic/effect.gleam", 132). ?DOC( " Most Lustre applications need to return a tuple of `#(model, Effect(message))`\n" " from their `init` and `update` functions. If you don't want to perform any\n" " side effects, you can use `none` to tell the runtime there's no work to do.\n" ). -spec none() -> effect(any()). none() -> {effect, [], []}. -file("src/agnostic/effect.gleam", 167). ?DOC( " Construct your own reusable effect from a custom callback. This callback is\n" " called with a `dispatch` function you can use to send messages back to your\n" " application's `update` function.\n" "\n" " Example using the `window` module from the `plinth` library to dispatch a\n" " message on the browser window object's `\"visibilitychange\"` event.\n" "\n" " ```gleam\n" " import agnostic/effect.{type Effect}\n" " import plinth/browser/window\n" "\n" " type Model {\n" " Model(Int)\n" " }\n" "\n" " type message {\n" " FetchState\n" " }\n" "\n" " fn init(_flags) -> #(Model, Effect(message)) {\n" " #(\n" " Model(0),\n" " effect.from(fn(dispatch) {\n" " window.add_event_listener(\"visibilitychange\", fn(_event) {\n" " dispatch(FetchState)\n" " })\n" " }),\n" " )\n" " }\n" " ```\n" ). -spec from(fun((fun((GIY) -> nil)) -> nil)) -> effect(GIY). from(Effect) -> Task = fun(Actions) -> Dispatch = erlang:element(2, Actions), Effect(Dispatch) end, {effect, agnostic@internals@constants:singleton_list(Task), erlang:element(3, {effect, [], []})}. -file("src/agnostic/effect.gleam", 192). ?DOC( " Construct an effect deferred to a platform-declared phase. This is the\n" " platform-author API: platform modules wrap it with typed constructors\n" " (e.g. [`dom.before_paint`](./platform/dom.html#before_paint) or\n" " [`opentui/effect.after_flush`](./platform/opentui/effect.html#after_flush)),\n" " pairing each constructor with a [`Phase`](./platform.html#Phase) of the\n" " same name in that platform's `phases` declaration. Application code should\n" " prefer those typed constructors.\n" "\n" " In addition to the `dispatch` function, your callback will also be provided\n" " with the root of your app or component as provided by the platform.\n" "\n" " > **Note**: if the running platform does not declare `phase`, the effect is\n" " > silently dropped and never run. This is what happens to all deferred\n" " > effects in server components, which declare no phases.\n" ). -spec deferred( binary(), fun((fun((GJA) -> nil), gleam@dynamic:dynamic_()) -> nil) ) -> effect(GJA). deferred(Phase, Effect) -> Task = fun(Actions) -> Root = (erlang:element(5, Actions))(), Dispatch = erlang:element(2, Actions), Effect(Dispatch, Root) end, {effect, erlang:element(2, {effect, [], []}), agnostic@internals@constants:singleton_list({Phase, Task})}. -file("src/agnostic/effect.gleam", 212). ?DOC(false). -spec event(binary(), gleam@json:json()) -> effect(any()). event(Name, Data) -> Task = fun(Actions) -> (erlang:element(3, Actions))(Name, Data) end, {effect, agnostic@internals@constants:singleton_list(Task), erlang:element(3, {effect, [], []})}. -file("src/agnostic/effect.gleam", 220). ?DOC(false). -spec select( fun((fun((GJE) -> nil), gleam@erlang@process:subject(any())) -> gleam@erlang@process:selector(GJE)) ) -> effect(GJE). select(Sel) -> Task = fun(Actions) -> Self = gleam@erlang@process:new_subject(), Selector = Sel(erlang:element(2, Actions), Self), (erlang:element(4, Actions))(Selector) end, {effect, agnostic@internals@constants:singleton_list(Task), erlang:element(3, {effect, [], []})}. -file("src/agnostic/effect.gleam", 251). ?DOC( " Provide a context value to child components in the DOM that this Lustre app\n" " didn't render. This occurs in components with that render one or more ``\n" " elements in their `view` function.\n" " \n" " Once a value for the given key has been provided, children can [`subscribe`](#subscribe)\n" " to changes and receive updates any subsequent times `provide` is called with\n" " the same key. This facilitates parent-child communication even in cases where\n" " the parent doesn't own the child element directly. \n" " \n" " **Note**: This is one half of the WCCG [Context Protocol](https://github.com/webcomponents-cg/community-protocols/blob/main/proposals/context.md)\n" " and will work in tandem with not just Lustre components but any third-party\n" " Web Component that implements the [`context-request` event](https://github.com/webcomponents-cg/community-protocols/blob/main/proposals/context.md#the-context-request-event).\n" ). -spec provide(binary(), gleam@json:json()) -> effect(any()). provide(Key, Value) -> Task = fun(Actions) -> (erlang:element(6, Actions))(Key, Value) end, {effect, agnostic@internals@constants:singleton_list(Task), erlang:element(3, {effect, [], []})}. -file("src/agnostic/effect.gleam", 275). ?DOC( " Subscribe to changes for a context value provided by a parent element in the\n" " DOM. This effect will decode the context value from the first parent element\n" " that has _already provided_ a context for this key at least once. Once a\n" " subscription is set up, any changes to the context value will trigger additional\n" " messages to be dispatched with the new decoded value.\n" " \n" " If no parent elements have provided a context for the given key at the time\n" " this effect is run, no subscription is set up even if a parent later provides\n" " a context for this key.\n" " \n" " **Note**: Pay attention to timing and lifecycle differences between applications\n" " and components. Components that need to subscribe to a context should make sure\n" " this effect is called _after_ the component has [connected](./component.html#on_connect).\n" " \n" " **Note**: This is one half of the WCCG [Context Protocol](https://github.com/webcomponents-cg/community-protocols/blob/main/proposals/context.md)\n" " and will work in tandem with not just Lustre components and applications, but\n" " any third-party Web Component that acts as a [context provider](https://github.com/webcomponents-cg/community-protocols/blob/main/proposals/context.md#context-providers).\n" ). -spec subscribe(binary(), gleam@dynamic@decode:decoder(GJL)) -> effect(GJL). subscribe(Key, Decoder) -> Task = fun(Actions) -> (erlang:element(7, Actions))(Key, Decoder) end, {effect, agnostic@internals@constants:singleton_list(Task), erlang:element(3, {effect, [], []})}. -file("src/agnostic/effect.gleam", 284). ?DOC( " Unsubscribe from a context [`subscription`](#subscribe) that was previously\n" " set up for this key.\n" ). -spec unsubscribe(binary()) -> effect(any()). unsubscribe(Key) -> Task = fun(Actions) -> (erlang:element(8, Actions))(Key) end, {effect, agnostic@internals@constants:singleton_list(Task), erlang:element(3, {effect, [], []})}. -file("src/agnostic/effect.gleam", 304). ?DOC( " Batch multiple effects to be performed at the same time.\n" "\n" " > **Note**: The runtime makes no guarantees about the order on which effects\n" " > are performed! If you need to chain or sequence effects together, you have\n" " > two broad options:\n" " >\n" " > 1. Create variants of your `message` type to represent each step in the sequence\n" " > and fire off the next effect in response to the previous one.\n" " >\n" " > 2. If you're defining effects yourself, consider whether or not you can handle\n" " > the sequencing inside the effect itself.\n" ). -spec batch(list(effect(GJQ))) -> effect(GJQ). batch(Effects) -> gleam@list:fold( Effects, {effect, [], []}, fun(Acc, Eff) -> {effect, gleam@list:fold( erlang:element(2, Eff), erlang:element(2, Acc), fun gleam@list:prepend/2 ), gleam@list:fold( erlang:element(3, Eff), erlang:element(3, Acc), fun gleam@list:prepend/2 )} end ). -file("src/agnostic/effect.gleam", 351). -spec do_comap_select( actions(GKI), gleam@erlang@process:selector(GKK), fun((GKK) -> GKI) ) -> nil. do_comap_select(Actions, Selector, F) -> (erlang:element(4, Actions))(gleam_erlang_ffi:map_selector(Selector, F)). -file("src/agnostic/effect.gleam", 336). -spec do_comap_actions(actions(GKE), fun((GKG) -> GKE)) -> actions(GKG). do_comap_actions(Actions, F) -> {actions, fun(Message) -> (erlang:element(2, Actions))(F(Message)) end, erlang:element(3, Actions), fun(Selector) -> do_comap_select(Actions, Selector, F) end, erlang:element(5, Actions), erlang:element(6, Actions), fun(Name, Decoder) -> (erlang:element(7, Actions))( Name, gleam@dynamic@decode:map(Decoder, F) ) end, erlang:element(8, Actions)}. -file("src/agnostic/effect.gleam", 327). -spec do_map(list(fun((actions(GJY)) -> nil)), fun((GJY) -> GKB)) -> list(fun((actions(GKB)) -> nil)). do_map(Effects, F) -> gleam@list:map( Effects, fun(Effect) -> fun(Actions) -> Effect(do_comap_actions(Actions, F)) end end ). -file("src/agnostic/effect.gleam", 318). ?DOC( " Transform the result of an effect. This is useful for mapping over effects\n" " produced by other libraries or modules.\n" "\n" " > **Note**: Remember that effects are not _required_ to dispatch any messages.\n" " > Your mapping function may never be called!\n" ). -spec map(effect(GJU), fun((GJU) -> GJW)) -> effect(GJW). map(Effect, F) -> {effect, do_map(erlang:element(2, Effect), F), gleam@list:map( erlang:element(3, Effect), fun(Entry) -> {erlang:element(1, Entry), fun(Actions) -> (erlang:element(2, Entry))(do_comap_actions(Actions, F)) end} end )}. -file("src/agnostic/effect.gleam", 381). ?DOC(false). -spec perform( effect(GKM), fun((GKM) -> nil), fun((binary(), gleam@json:json()) -> nil), fun((gleam@erlang@process:selector(GKM)) -> nil), fun(() -> gleam@dynamic:dynamic_()), fun((binary(), gleam@json:json()) -> nil), fun((binary(), gleam@dynamic@decode:decoder(GKM)) -> nil), fun((binary()) -> nil) ) -> nil. perform(Effect, Dispatch, Emit, Select, Root, Provide, Subscribe, Unsubscribe) -> Actions = {actions, Dispatch, Emit, Select, Root, Provide, Subscribe, Unsubscribe}, gleam@list:each(erlang:element(2, Effect), fun(Run) -> Run(Actions) end).