Live sync and reloads

In the playground, the editor and the output stay in sync as you work. Most edits are hot-patched into the running page, so its state survives; a few edits, and the F5 key, reload the output. Changes your page makes to itself can flow back into the source. This guide shows the controls, lists exactly when the output reloads, and shows how to steer both directions. For the ideas behind it, read The IOSync model.

The controls

Note: Sync belongs to the playground. A deployed page on RTEdge.net contains none of it, and the sync attributes stay in its HTML as inert attributes. See Playground and edge differences.

Both directions have a Live toggle and a Once command. Load is separate: it reloads the output completely.

The Sync menu

The Sync section of the File menu (the logo menu, Alt+F / ⌥+F) has two submenus. Four commands are also in the F1 palette and in the editor's context menu: the two live toggles, Toggle Locate and Toggle Shadow Source Materialization.

Menu itemKeysWhat it does
Input → Output ▸ Live ▸ SyncAlt+I / ⌃+⌥+IA checkbox, on by default. Applies each edit to the output as you type. Turning it on runs one sync at once.
Input → Output ▸ Once ▸ SyncF4Applies the source to the output once, as a hot patch, also when live sync is off.
Input → Output ▸ Once ▸ LoadF5Reloads the output completely. Output state is not preserved.
Input ← Output ▸ Live ▸ SyncAlt+O / ⌥+OA checkbox, on by default. Writes changes in the output back into the source as the DOM changes, while the editor does not have focus. Turning it on pulls the output once.
Input ← Output ▸ Once ▸ SyncF6Pulls the output into the editor once. It works with the editor focused, and with live sync off.

Turning live input → output sync on is a normal sync: it hot-patches, or reloads if a reload trigger is present. Turning live output → input sync off stops reverse sync in every output. Both toggles are saved in the URL.

F4 to F8 work with focus in the editor or in the output. F5 reloads only the output, never the playground page: Ctrl+R / ⌘+R reloads the playground.

Note: The Load item in the Edge menu is a different command, Code: Revert Changes (Ctrl+I R / ⌘+I R). It reverts the editor to the last deployment and has nothing to do with Sync ▸ Load.

The tooltips of the menu say that output state is preserved "except on editing <script>". Script edits are the most common reason for a reload, but not the only one: see When the output reloads.

The status pill

The logo shows which directions are live, and flashes what just happened.

PillMeaning
↔Both directions are live.
→Only input → output is live.
←Only output → input is live.
×Neither is live.

The right arrow flashes green after a hot patch that changed at least one output, and amber after a Load. It turns red when an input → output sync fails, and stays red until the next successful patch or Load. The left arrow flashes green when a reverse pull starts. The arrow of the direction that is not active, which follows editor focus, is dimmed.

An input → output problem also appears as a red notification with the engine status, for example i→o invalid: tag or i→o failed: …. The next successful patch clears it.

Controlling sync from the URL and from scripts

These keys in the playground URL hold the sync state. A key is left out of the URL while its value equals the value at startup, so a link that ends in &io=0 opens with live input → output sync off.

KeyDefaultControls
io=1 or io=01Live input → output sync.
oi=1 or oi=01Live output → input sync.
sm=1 or sm=00Locate. See Locating source with F8.
srs=1 or srs=00Shadow Source Materialization (F7).

Experimental From io.eval.js or the playground tab's DevTools console, you can drive sync with these calls. They are not part of a stable API, and they may change. Run one command at a time. See sync for the objects.

js
io.sync = false                                                              // stop live input → output
oi.sync = false                                                              // stop live output → input
await sync.output({ force: true, io: { checked: true } })                    // F4: patch now, or reload if a trigger is present
await sync.output({ force: true, io: { checked: true }, trigger: 'F5' })     // F5: Load
await sync.editor({ force: true, oi: { checked: true }, trigger: 'F6' })     // F6: pull the output once, as its own undo step

sync.output() resolves to { length: n } with the number of outputs that changed, to { skipped: true } when live sync is off and you did not pass io: { checked: true }, to { superseded: true } when a newer edit exists, and to { e: status } on a failure.

How an edit reaches the output

With live input → output sync on, every editor change that carries a concrete change list is forwarded to the output, wherever the focus is. That covers typing, paste, undo and redo, writes to 📄.html during an Agent Folder Share, and other programmatic edits. The sync lane coalesces them, and IOSync applies each as a hot patch or, when it must, as a reload.

Hot patches

Everything that is not an executable script or a reload trigger updates live, with no reload:

  • text, attributes and inline style;
  • <style> text and <link href>;
  • inline event-handler attributes, which the browser recompiles from the new attribute value;
  • <title> and the contents of a <template>;
  • data-block scripts, such as type=text/markdown and application/json, and <script pure> text.

A hot patch never runs a script. It changes only what your edit changed: a value your script set stays, unless you edit that field in the source. Focus, selection, scroll position, media playback, canvas pixels, JavaScript variables and listeners are not touched, and typed form input survives. See What survives an edit.

Edits that are held back

Unfinished markup is held back. The output keeps its last good state, the playground shows i→o invalid: <reason>, and sync resumes on the first valid keystroke.

ReasonCause
tag<x or </x without >. An unclosed end tag such as </p also reports tag.
nested-tagA new <x or </ while a tag is still open.
quoteAn attribute quote that is never closed.
comment<!-- without -->.
CDATA<![CDATA[ without ]]>.
decl<!… or <?… without >.
templateA <template> without its end tag, or invalid content inside a template.

An unclosed <script>, <style>, <textarea> or <title> is valid: the rest of the source becomes its text.

Edits that fail or are refused

Every full morph is atomic. If its verification fails, or anything throws, IOSync restores the output to the last accepted state and keeps the last accepted source. The status reads i→o failed: <message>, and the editor is never changed. Two structural edits are refused before anything is touched:

  • Adding a source shadow root (<template shadowrootmode>) to a host that is already live fails with i→o failed: Adding a shadow root to an existing host cannot be rolled back. Press F5 to apply it.
  • An ambiguous source element that a script removed fails with A runtime-removed source element has ambiguous target correspondence.

Fast typing

The sync lane holds at most one pending job. A newer edit replaces it, and a drain runs on the next animation frame or after 50 ms, whichever comes first. On heavy pages, fast typing or key repeat can therefore skip intermediate output states, but the final state always arrives.

When the output reloads

A hot patch cannot do everything. It never runs scripts, and exported files reach the output's service worker only on a full reload. So some edits, and some states of your source, reload the output.

The triggers

TriggerThe output reloads when
LoadYou press F5 or choose Sync ▸ Input → Output ▸ Once ▸ Load. It reloads even when live input → output sync is off.
Reload markersAny <script reload> (of any type) or any element with sync=reload exists in the source. Then every edit anywhere is a full reload. The value match is exact and case-sensitive, and the Boolean reload counts only on <script>.
Executable scriptsThe set of executable scripts changes. A script counts when it is not pure and its type is one that runs code. The comparison covers the src URLs and the whole outer HTML of those scripts, attributes and text included: editing inside a script, adding, removing or reordering scripts, and adding or removing pure all reload.
Undeclared custom elementsAt least one element with a hyphen in its tag name is not declared sync-safe. Then every edit reloads, and removing the last such element reloads once more.
<base>The href of the first <base> element is added, removed or changed.
iumThe number of whitespace-preceded ium tokens in the source changes. These are the attributes that switch WebPDF to its high-fidelity renderer.
Output initializationThe first sync after the playground loads, also with #io=0; a new output window opens (only that window initializes); you run Import URL… or Paste URL Content…; or the playground recovers from an output initialization error. Before an o.input lookup or an o.eval.js run, a failed initialization is retried once.

Each trigger fires on the change relative to the last synced source. The two that reload on every edit while they hold, reload markers and undeclared custom elements, are the exception. The script types that count as executable are listed in <script>, and the ways to declare a custom element are in Declare your custom elements.

What does not reload the output

  • Anything inside a <template>, including scripts, custom elements, and reload or io:sync markers inside <template o:=page.html> and <template shadowrootmode>.
  • Data-block scripts.
  • <script pure> Experimental.
  • Edits to <style> and <link>.
  • Customized built-in elements, such as <button is=x-y>, because their tag names have no hyphen.

What a reload resets and keeps

A full reload resets JavaScript state, the DOM, form values and runtime DOM, and it clears the console and network panels. It keeps:

  • the output's scroll position;
  • the output's own URL #hash, but only for top-level output windows (a tab or a popup), not for the embedded output;
  • the output settings, which are applied again;
  • the storage of the output's origin, such as localStorage and IndexedDB;
  • the module-level state of a <script type=worker> handler, until the worker code changes or the output's service worker restarts: the compiled handler is cached per session and exact worker source.

Exported files refresh only on a reload

Exported files reach the output's service worker only during a full reload. That covers o:= files, worker:fs assets (env.FS), worker code, and the copies published to sibling routes. After a hot patch, /name.ext, env.FS and the sibling copies keep the content of the previous load. Three fixes:

  • press F5;
  • put reload on the exporting <script>;
  • add <meta sync=reload>.

See Exporting files and Make every edit reload.

Pages that navigate inside the output

Only the output's root page, /, is served with the sync runtime. Exported files, including exported .html pages such as <template o:=page.html>, are served exactly as stored. If a link or a location change inside the output takes you to such a page, IOSync, reverse sync and Locate are absent on it.

A page that reloads itself, or navigates to /, receives the source as of the last full reload, not the current editor text. The next edit or F4 patches the page forward, and F5 reloads it with the current source.

Steering reloads

You choose when the output reloads. Declare the elements that are safe to patch, mark what must always reload, or let a script's text change without restarting it.

Declare your custom elements

While one undeclared custom element exists outside templates, every edit reloads the output. A sync-safe custom element never forces a reload. An element qualifies in four ways:

  • its tag is one of the built-in tags pdf-file, pdf-page, zero-md and di-ff;
  • its tag starts with a built-in prefix, such as sl- or sp-;
  • its tag, or a prefix of it, is listed in a <meta io:sync>;
  • the instance carries sync=pure Experimental.

Here <tick-tock> is declared. Editing the paragraph, its class or the <style> hot-patches the page, and the clock keeps the time it showed when it connected. Editing the <script> reloads the output. Delete the <meta> line and every edit reloads, because <tick-tock> is then undeclared.

html
<meta io:sync=tick-tock>
<style>:root { color-scheme: light dark } p { color: light-dark(oklch(45% .2 250), oklch(80% .12 250)) }</style>
<p class=lead>Edit me: text, class and CSS update without a reload.</p>
<tick-tock></tick-tock>
<script type=module>
  customElements.define('tick-tock', class extends HTMLElement {
    #root = this.attachShadow({ mode: 'closed' })
    connectedCallback() { this.#root.textContent = new Date().toLocaleTimeString() }
  })
</script>

An element should be declared only if it does not pollute its own light DOM. Writing IOSync-safe components shows how to build one that does not.

Make every edit reload

Put reload on a <script> of any type, or sync=reload on any element, and every edit becomes a full reload. Here the data block exports /notes.txt. With reload, the file and the frame always show the current text. Without it, the frame keeps the file of the last load.

html
<script reload type=text/plain o:=notes.txt>Edit me and the frame follows.</script>
<iframe src=notes.txt></iframe>

A page-wide alternative is <meta sync=reload>. Sites built in the playground use both forms: rtedge.net puts reload on its module script, and new.webpdf.pro puts sync=reload on its viewport <meta>. See <script> and sync.

Let a script edit hot-patch

Experimental A <script pure> leaves the set of executable scripts. Editing it hot-patches the script's text in the DOM, but the code does not run again until the next full reload, and the old code keeps running. Adding or removing pure reloads once.

In this counter, the edit applies to the script's text and the new code runs only after F5. The oi:exclude attribute keeps the ticking value from being written into the source every second.

html
<output id=n oi:exclude>0</output>
<script pure>
  setInterval(() => n.value = +n.value + 1, 1000)
</script>

Note: pure and sync=pure are experimental. They work, but they may change.

From the page back to the source

With live output → input sync on, changes the page makes to itself are turned into exact edits of the source. The engine observes the output's <body> and every open shadow root it can reach.

What flows back

  • Elements, text and comments that scripts or the user add are adopted into the source, at the matching position.
  • Removing a node that came from the source deletes its range of the source.
  • Attribute changes are written back, including open on <details> and <dialog>, and attributes and classes that a component sets on its host.
  • Text changes are written back, for example typing in a contenteditable element.

Some changes never flow back: writes that do not change a value, changes inside oi:exclude regions, changes inside runtime shadow roots (unless Shadow Source Materialization is on), and what the engine's own hot patches cause. Live form properties are not written back either: a typed input.value, a checked that the user toggled and a <select> choice change no attribute. Changes to <head> and to the attributes of <html> never sync back, because only <body> is observed.

When you turn live output → input sync on, and after every output initialization while it is on, including each Load, the engine takes a full snapshot. DOM that your scripts created while the page loaded is adopted into the source as soon as the editor lets the change in. Elements that keep their light DOM still do not cause this: see Writing IOSync-safe components.

When changes are applied

The editor holds changes that arrive while it has focus, and applies them when it loses focus and after the next input → output sync. The tooltip of the menu item says that live output → input sync "only works when the input is not in focus". While an Agent Folder Share is active, the editor counts as the driving side, so live changes from the output wait until you stop sharing. F6 still pulls the output once.

The text replaces the whole editor model in one edit. Scroll position and selections are restored, but folds can be lost. Live changes merge into the previous undo step. A change that comes back from one output is hot-patched into the other outputs, and the output that made it is not morphed again.

Pulling the output once

F6 takes a full snapshot of the current output, with the current Shadow Source Materialization setting, and writes it to the editor as its own undo step. It works with live output → input sync off, and it is idempotent: pressing it again changes nothing. It trims trailing whitespace at the end of the document, and if the result has no <meta … io:sync…> line, it puts the editor's existing io:sync meta lines back at the top. Live changes do neither.

How the source is rewritten

Only changed ranges are rewritten. Untouched whitespace, attribute quoting, comments, entity spellings and templates stay byte-identical. Changed or new parts follow these rules:

  • An empty attribute value is written as a bare name. A value is unquoted unless it contains whitespace, ", ', a backtick, =, < or >. A value that contains " but no ' gets single quotes. Any other quoted value gets double quotes, with &quot; inside.
  • A new attribute goes just before the start tag's > (or before />). A removed attribute leaves with its leading whitespace.
  • A changed text node is rewritten whole, with &, < and > escaped, so an entity such as &nbsp; in it becomes the literal character.
  • New elements are serialized compactly in place: lowercase HTML tag names, no added newlines or indentation, and void elements without an end tag or slash.
  • Adopted nodes go right after the source of their previous sibling. Failing that, they go before the source of their next sibling, then right after the parent's start tag. Top-level additions to <body> go at the end of the document.

Turning it off

Turning live output → input sync off cancels all queued reverse work in every output and discards unaccepted proposals. The live DOM is not touched.

Keys at a glance

KeysCommand
Alt+I / ⌃+⌥+IToggle Input → Output Sync
Alt+O / ⌥+OToggle Input ← Output Sync
F4Sync once, input → output
F5Load: reload the output
F6Sync once, output → input
F7Toggle Shadow Source Materialization
F8Toggle Locate

See Keyboard shortcuts and Commands for every key.

Common situations

What you seeWhy, and what to do
Every edit reloads the output.A reload marker, an undeclared custom element or another trigger is present. Declare your elements with <meta io:sync>, or remove the marker.
The output shows an old exported file.Exports refresh only on a full reload. Press F5, or add reload or sync=reload.
A red i→o invalid notification appears while you type.The markup is unfinished. Finish the tag, comment or quote, and sync resumes.
i→o failed: Adding a shadow root to an existing host…A shadow root cannot be added to a live host. Press F5.
Content your script created shows up in your source.Reverse sync adopts light-DOM changes. Put the generated markup in a closed shadow root, mark its container with oi:exclude, or turn live output → input sync off with Alt+O / ⌥+O.
A change in the page does not reach the editor while you type.The editor holds output → input changes while it has focus. Click out of the editor, or press F6.
A script edit does not take effect.The script is pure, so its code runs again only after a full reload. Press F5.

More help is in Troubleshooting.

See also