#background jobs

MVC::Keayl::Job runs work now or later. A subclass implements perform; perform-now runs it immediately, and perform-later enqueues it on the configured adapter. Positional and named arguments pass straight through. With no adapter configured, perform-later runs inline, so a job is always runnable without setup.

class WelcomeJob is MVC::Keayl::Job {
  method perform($user-id) {
    # send a welcome email, warm a cache, ...
  }
}

# runs now, returns the result
WelcomeJob.perform-now(42);

# enqueues on the adapter
WelcomeJob.perform-later(42);

The adapter decides what "later" means. Inline runs on enqueue, Test collects jobs without running them, Async runs each on its own thread, and the Database adapter persists each job through a pluggable store.

MVC::Keayl::Job.queue-adapter(
  MVC::Keayl::Job::QueueAdapter::Async.new);

my $adapter =
  MVC::Keayl::Job::QueueAdapter::Test.new;
MVC::Keayl::Job.queue-adapter($adapter);

# collected, not run
WelcomeJob.perform-later(42);

# now it runs
$adapter.perform-all;

#mailers

MVC::Keayl::Mailer builds email the way a controller builds a response: an action renders HAML views into the parts of a message, and a delivery method sends it. mail renders an html and a text part when both templates exist, making the message multipart.

class NoticeMailer is MVC::Keayl::Mailer {
  method welcome($user) {
    self.mail(
      to      => $user.email,
      from    => 'noreply@example.com',
      subject => 'Welcome',
      locals  => %( name => $user.name ),
    );
  }
}

NoticeMailer.default(
  from => 'noreply@example.com');

my $mailer = NoticeMailer.new(
  view-renderer => $view,
  delivery => $delivery);
$mailer.deliver('welcome', $user);

Views are named for the mailer and action: NoticeMailer#welcome renders under notice_mailer/welcome.html.haml and notice_mailer/welcome.text.haml. Pair a mailer with a job to deliver in the background.

#caching and conditional GET

fresh-when sets validators and renders a 304 Not Modified when the request shows the client already has the current version; is-stale is its inverse. expires-in sets Cache-Control, and a pluggable cache store offers a low-level key-value API with a fetch that computes-and-stores on a miss.

method show {
  self.render('show')
    if self.is-stale(
      etag => $post,
      last-modified => $post.updated-at);
}

# Cache-Control: public, max-age=3600, must-revalidate
self.expires-in(
  3600,
  public => True,
  must-revalidate => True);

$store.fetch(
  'user/1',
  { load-user(1) },
  expires-in => 300);
$store.increment('visits', 1, expires-in => 3600);
$store.delete-matched(/^user\//);

#content negotiation

A controller action serves several formats with respond-to. The format is taken from the path extension if present, otherwise negotiated from the Accept header; the first declared format is the default, and a request that matches none gets 406.

method show {
  self.respond-to([
    html => { self.render('show') },
    json => { self.render(:json($post.to-hash)) },
  ]);
}

With Accept: text/html, the html block runs and renders the template.

curl -H 'Accept: text/html' localhost:3000/posts/7
200 OK Content-Type: text/html; charset=utf-8 <h1>Deploying web apps with Raku</h1>

The .json extension forces JSON, so the json block runs and renders the serialized post.

curl localhost:3000/posts/7.json
200 OK Content-Type: application/json {"id":7,"title":"Deploying web apps with Raku"}
Device variants. A format block may be a map of variant names to blocks, so phone renders show_phone while any falls back to show. MVC::Keayl::APIController is the JSON-first base for API-only apps.

#live streaming and SSE

A live action writes the response body incrementally on its own thread, while the adapter pulls the chunks through to the client. live runs a block, passing the controller and a stream; the block writes chunks with write, and the framework closes the stream when it returns. The action returns as soon as the block is spawned, so the dispatch loop is not blocked.

method feed {
  self.live(-> $controller, $stream {
    CATCH {
      when X::MVC::Keayl::Live::ClientDisconnected
      {
        cleanup();
      }
    }

    loop {
      $stream.write(next-chunk());
    }
  });
}

When the client goes away, the next write raises X::MVC::Keayl::Live::ClientDisconnected, which the action catches to release resources and stop producing.

#websockets

MVC::Keayl::Cable is an ActionCable-style abstraction for real-time messaging. A connection holds one client, channels group its subscriptions, and a pub/sub backend fans broadcasts out to everyone on a stream. Subclass a channel, stream from a named broadcasting in subscribed, and add a method per action a client can invoke.

class ChatChannel is MVC::Keayl::Cable::Channel {
  method subscribed {
    self.stream-from(
      'room:'
        ~ self.connection.identifiers<room>);
  }

  method speak(%data) {
    self.broadcast-to(
      'room:'
        ~ self.connection.identifiers<room>,
      %data<message>);
  }
}

stream-from subscribes the channel to a broadcasting, transmit sends straight to this client, and broadcast-to publishes to a stream, reaching every subscribed connection. perform dispatches a client message to an action method, and only methods defined on the channel are callable, so framework methods cannot be invoked from the wire. A connection declares its identifiers with identified-by and verifies them in connect, rejecting with reject-unauthorized-connection.

#internationalization

MVC::Keayl::I18n loads locale files into one store, looks up translations by dotted key, interpolates and pluralizes them, and localizes dates, times, and numbers. load-locales reads every .yml, .yaml, and .json file in a directory and merges them by top-level locale.

my $i18n = MVC::Keayl::I18n.new(
  default-locale    => 'en',
  available-locales => <en fr ru>,
  use-fallbacks     => True,
);

$i18n.load-locales('config/locales'.IO);

# "Hello Ada"
$i18n.translate('greeting', name => 'Ada');

# "3 apples"
$i18n.t('apples', count => 3);
More in the box. Logging, instrumentation and notifications, error reporting with a developer exception page, an asset pipeline, WebSockets, Active Storage, Action Text, and Action Mailbox round out the stack. See the documentation for the rest.