Module: MaquinaStream
- Extended by:
- Registries
- Defined in:
- lib/maquina_stream.rb,
lib/maquina_stream/block.rb,
lib/maquina_stream/frame.rb,
lib/maquina_stream/engine.rb,
lib/maquina_stream/errors.rb,
lib/maquina_stream/export.rb,
lib/maquina_stream/themes.rb,
lib/maquina_stream/version.rb,
lib/maquina_stream/document.rb,
lib/maquina_stream/manifest.rb,
lib/maquina_stream/renderer.rb,
lib/maquina_stream/sanitizer.rb,
lib/maquina_stream/components.rb,
lib/maquina_stream/registries.rb,
lib/maquina_stream/streamable.rb,
lib/maquina_stream/broadcaster.rb,
lib/maquina_stream/configuration.rb,
lib/maquina_stream/renderer/fence.rb,
lib/maquina_stream/text_direction.rb,
lib/maquina_stream/component_cache.rb,
lib/maquina_stream/renderer/post_pass.rb,
lib/maquina_stream/components/contract.rb,
lib/maquina_stream/renderer/tag_blocks.rb,
lib/maquina_stream/renderer/view_context.rb,
app/helpers/maquina_stream/components_helper.rb,
app/controllers/maquina_stream/blocks_controller.rb,
app/controllers/maquina_stream/manifests_controller.rb,
app/controllers/maquina_stream/application_controller.rb,
lib/generators/maquina_stream/install/install_generator.rb,
lib/generators/maquina_stream/streamable/streamable_generator.rb
Overview
Server-rendered streaming markdown for Rails, over Turbo, with a repair path.
A model writes markdown a token at a time. The engine renders it on the server, broadcasts small patches as it grows, and reconciles the browser with the truth when frames go missing — which they do, because Action Cable offers no delivery guarantee. Only rendered HTML reaches the browser; the client never parses markdown.
The whole integration
class Message < ApplicationRecord
include MaquinaStream::Streamable
maquina_stream buffer: :content,
stream_for: ->(m) { [m.conversation, :messages] }
end
broadcaster = MaquinaStream::Broadcaster.new()
model.stream { |token| broadcaster.append(token) }
broadcaster.seal!
<%= MaquinaStream.render(message) %>
What a host owns
The engine resolves nothing and authorizes nothing on its own. Three things are the host's, and none of them has a default the engine could guess:
| Host responsibility | Where it goes |
|---|---|
| Looking a record up by its stream id | Configuration#find_stream |
| Deciding whether a request may see it | Configuration#authorize |
| Persisting the buffer, sequence and status | MaquinaStream::Streamable |
With no authorize configured, every repair request is refused. That is
deliberate: an engine that guesses is an engine that leaks.
Where to look next
| Object | Responsibility |
|---|---|
| MaquinaStream::Configuration | Every option, its default and what changing it costs |
| MaquinaStream::Streamable | The host contract, and the macro that generates it |
| MaquinaStream::Broadcaster | Frame coalescing and Turbo Stream emission |
| MaquinaStream::Renderer | Markdown in, sanitized HTML out. A pure function |
| MaquinaStream::Document | Splits a rendered message into blocks and decides which may freeze |
| MaquinaStream::Block | One top-level block: id, markdown, HTML, digest |
| MaquinaStream::Frame | One broadcast: what changed since the last one |
| MaquinaStream::Manifest | What the browser is told the message currently is |
| MaquinaStream::Sanitizer | Allowlist plus URL hardening, the last pass before output |
| MaquinaStream::Export | A whole message, back out as markdown |
| MaquinaStream::TextDirection | Which way a piece of text reads |
| MaquinaStream::Components::Contract | The engine's half of a vendored component |
Longer-form documentation ships in docs/: getting-started.md,
configuration.md, streaming.md, repair.md, registries.md,
javascript.md, security.md and deferred-renderers.md.
Defined Under Namespace
Modules: Components, ComponentsHelper, Export, Generators, Registries, Streamable, TextDirection, Themes Classes: ApplicationController, Block, BlocksController, Broadcaster, ComponentCache, Configuration, Document, Element, Engine, Fence, Frame, Manifest, ManifestsController, Renderer, Sanitizer, Tag
Constant Summary collapse
- VENDORED_COMPONENTS =
Components destined for maquina_components, vendored inside the engine for now. Everything renders through MaquinaStream::Components, so extraction is a matter of publishing the partial there and dropping the name from here.
%i[attachment code_block suggestion snippet].freeze
- Error =
Base class for everything this engine raises. Rescue it to catch all of them at once.
Class.new(StandardError)
- ContractError =
Raised when a host model does not satisfy the MaquinaStream::Streamable contract. The message names the method, the column and the class, so the fix is in the error rather than in a document.
Class.new(Error)
- ConfigurationError =
Raised when a required host seam has not been configured —
find_stream, in practice.authorizedenies instead of raising, because denial is the safe answer. Class.new(Error)
- VERSION =
"0.1.0"
Class Method Summary collapse
-
.config ⇒ Object
The current Configuration.
-
.configure {|config| ... } ⇒ Object
Configures the engine.
-
.render(record, config: self.config) ⇒ Object
Renders a whole message to HTML, cached once it is sealed.
-
.reset_configuration! ⇒ Object
Throws away the configuration and starts again from the defaults.
Methods included from Registries
elements, fences, register_element, register_fence, register_tag, reset_registries!, tags
Class Method Details
.config ⇒ Object
The current Configuration. Memoized; the same object configure
yields.
95 96 97 |
# File 'lib/maquina_stream.rb', line 95 def config @config ||= Configuration.new end |
.configure {|config| ... } ⇒ Object
Configures the engine. Yields the Configuration and returns it.
MaquinaStream.configure do |c|
c.find_stream = ->(sid) { Message.find_by(id: sid) }
c. = ->(record, request) { record.conversation.readable_by?(request) }
end
Every option, with its default and the consequence of changing it, is documented on Configuration. Call this once from an initializer: configuration is global and read on every render, so changing it mid-stream changes what later frames of an open message look like.
112 113 114 115 |
# File 'lib/maquina_stream.rb', line 112 def configure yield config config end |
.render(record, config: self.config) ⇒ Object
Renders a whole message to HTML, cached once it is sealed. Returns an
html_safe String.
<%= MaquinaStream.render(message) %>
This is the history path — one call, one finished message on a page. It is not what a live stream uses: an open stream goes out frame by frame through Broadcaster. Pagination stays the host's; the engine renders the messages the host chose, in the order it chose them.
record must satisfy MaquinaStream::Streamable. config: defaults to
the global MaquinaStream.config.
A sealed message is immutable, so its HTML is a pure function of its buffer and can be cached by digest — which is what makes a page of history cheap: re-rendering fifty finished messages on every page load is work nobody asked for. An open message is never cached; it is about to change.
The digest is of the buffer, so a host that edits a message gets a new key rather than a stale render.
147 148 149 150 151 152 153 154 155 156 157 158 159 |
# File 'lib/maquina_stream.rb', line 147 def render(record, config: self.config) markdown = record.maquina_stream_buffer # Through Document, not Renderer. Renderer produces the HTML; Document is # what stamps each block with the id and digest the DOM contract requires, # and without those a page cannot be repaired at all — ms-repair would # have nothing to compare a manifest against. return document_html(record, markdown, config) if record.maquina_stream_open? ComponentCache.fetch("document", record.maquina_stream_id, Digest::SHA256.hexdigest(markdown.to_s)) do document_html(record, markdown, config) end end |
.reset_configuration! ⇒ Object
Throws away the configuration and starts again from the defaults.
For tests. A host that calls this in production loses its find_stream
and authorize seams, and every repair request after it is refused.
121 122 123 |
# File 'lib/maquina_stream.rb', line 121 def reset_configuration! @config = Configuration.new end |