Skip to content

Service Lifecycle

This tutorial walks through every stage of a service lifecycle so you can build intuition for what happens under the hood.

Prerequisites

  • xs installed and on your PATH (see Installation)
  • Two terminal windows, both running Nushell with use xs.nu *

Serve

Start a store in terminal 1:

Terminal window
xs serve ./store

Monitor the stream

In terminal 2, start a live monitor so we can watch lifecycle frames as they appear:

Terminal window
.cat -f | each {
if $in.hash != null { insert content { .cas $in.hash } } else { } | print ($in | table -e)
}

Keep this running, every frame we discuss will show up here.

Create a service

Create a temporary file for the service to watch:

Terminal window
touch /tmp/log.txt

Now create a service that tails the file:

Terminal window
r#'{
run: {|| ^tail -F /tmp/log.txt | lines }
}'# | .append xs.service.log.create

Your monitor shows two frames:

-#-+-----------------topic----------------+---id---+-hash-+-----meta---
0 | xs.service.log.create | 03ab.. | ... |
1 | xs.service.log.active | 03ab.. | | source_id: 03ab..

xs.service.log.create carries the script in CAS. xs.service.log.active signals the pipeline is live; meta.source_id points back at the create.

See output

Write some lines to the file:

Terminal window
"hello\nworld\n" | save -a /tmp/log.txt

Two log.recv frames appear, one per line. (Note: recv/send are app-namespace data topics, not under xs..) Read the content back:

Terminal window
.last log.recv | .cas $in.hash
world

Auto-restart

Remove the file so tail exits:

Terminal window
rm /tmp/log.txt

No lifecycle frame is emitted, the service’s run-loop treats a natural completion as an internal hiccup and quietly restarts after 1 second. A new xs.service.log.active frame appears.

Recreate the file and write to it:

Terminal window
touch /tmp/log.txt
"back in business\n" | save -a /tmp/log.txt

A fresh log.recv frame appears. The service recovered automatically.

Hot reload

Append a new xs.service.log.create while the service is running:

Terminal window
r#'{
run: {|| ^tail -F /tmp/log.txt | lines | each {|line| $"[LOG] ($line)" } }
}'# | .append xs.service.log.create

The running service picks up the new create, stops the old pipeline, emits xs.service.log.replaced (with meta.update_id pointing at the new create), and a new xs.service.log.active follows for the reloaded task.

Verify the new behaviour:

Terminal window
"reloaded\n" | save -a /tmp/log.txt
Terminal window
.last log.recv | .cas $in.hash
[LOG] reloaded

The output now includes the [LOG] prefix.

What if the new script is broken?

If the replacement create has a parse error, the runtime emits xs.service.log.invalid and the previous service keeps running. On the next restart of xs, compaction also keeps the previous (good) create as the fallback. The system never lands in an empty state because of a typo. See the compaction algorithm for the rules.

Terminate

Stop the service explicitly:

Terminal window
.append xs.service.log.term

The monitor shows two frames:

  1. xs.service.log.term, your request
  2. xs.service.log.fin.term, the runtime ack confirming the stop

After fin.term, the service stays down. On the next restart of xs, it will not be started: compaction sees the fin.* and clears its slots.

Graceful shutdown

When xs itself stops (e.g. Ctrl+C), it emits an xs.stopping frame. Every running service sees this frame, interrupts its pipeline, and emits xs.service.<name>.stopped as its ack.

Restart the service so we can see this in action:

Terminal window
r#'{
run: {|| ^tail -F /tmp/log.txt | lines }
}'# | .append xs.service.log.create

Wait for xs.service.log.active to appear, then press Ctrl+C in terminal 1 (where xs serve is running).

The monitor shows:

  1. xs.stopping, emitted by xs before exit
  2. xs.service.log.stopped, the service acknowledging the shutdown

xs waits up to a few seconds for all services to finish before exiting. This gives services a chance to flush output and clean up resources.

Crucially, stopped is distinct from fin.*: compaction does not treat it as terminal. On the next start of xs, the service resumes (its create is still the latest known-good entry).

Recap

User inputs and runtime acks for a service:

EventDirectionWhen
xs.service.<name>.createuser -> streamYou start (or reload) the service
xs.service.<name>.termuser -> streamYou ask the service to stop
xs.service.<name>.activeruntime -> streamPipeline is live
xs.service.<name>.invalidruntime -> streamThe create’s script failed to parse
xs.service.<name>.fin.errorruntime -> streamRuntime crash; service stays down
xs.service.<name>.fin.termruntime -> streamStop ack for .term
xs.service.<name>.replacedruntime -> streamHot-reloaded by a newer .create
xs.service.<name>.stoppedruntime -> streamStopped because of xs.stopping

Plus the app-namespace data channels: <name>.recv (output, one frame per line), <name>.send (input, duplex services only).

For the full event vocabulary, the compaction algorithm, and the invariants the dispatcher honors, see the lifecycle reference.