Varendo

Application scopes

Bind surfaces to a host-authorized workspace and fence stale work across voluntary switches and revocation.

A scope binds each surface and its work to one authorized target, such as a workspace. A workspace ID selects a candidate — it never grants permission.

When you need this

1. Implement the binding

Create the scope at your app's composition root. The binding resolves the selector and authorizes the target using current host state. Every host.* callback is your code, so the selector alone grants nothing.

scope.ts

TypeScript example
import type { ScopeBinding } from '@varendo/runtime/scopes';

const binding: ScopeBinding = {
  resolve: (selector) => host.resolveWorkspace(selector),
  authorize: (resolution) => host.authorizeWorkspace(resolution),
  prepareActivation: (target, context) => host.acceptWorkspaceActivation(target, context),
  activate: (target, context) => host.commitWorkspaceActivation(target, context),
  deactivate: (active, reason) => host.releaseWorkspaceActivation(active, reason),
};

resolve maps the selector to a target. authorize re-checks permission. prepareActivation synchronously rechecks the target and captured revisions before acceptance. activate commits your side effects; deactivate reverses them. The full lifecycle is in the runtime package guide.

2. Create and attach the scope

attach starts the scope in its initial state.

TypeScript example
const workspace = runtime.createScope({ initial: { kind: 'workspace', id: 'acme' }, binding });
const detach = workspace.attach();

Every child result, cursor, proposal, action, subscription, and renderer address carries the activation it started under. A later activation with the same workspace ID is a new session. Work started under the old one cannot commit after a detour through another workspace.

3. Switch voluntarily

For a user-requested switch, call requestChange(selector).

TypeScript example
const result = await workspace.requestChange({ kind: 'workspace', id: 'acme-labs' });

If a form is dirty, the result is needs-input with save, discard, and stay choices. Your readLeaveState and beforeLeave callbacks drive that decision. While it is pending, the old authorized subtree stays labeled with its old workspace. A failed save or denied target leaves it in place. Once acceptance succeeds, the old activation is fenced before the new one starts effects.

4. Revoke forcibly

Call invalidate when access is lost:

TypeScript example
workspace.invalidate('logout'); // or 'revoked' | 'expired' | 'external-switch'

Invalidation masks the scope and fences children synchronously — even while a dirty-work guard is open. A recovery hook may navigate afterward but cannot delay the fence. Never show cached rows from the revoked activation as a failure fallback.

Keep layout inside one scope

Switching a workspace from single to split or compare changes presentation, not tenancy. Child surfaces keep stable identities and one owner each. A proposed composition with a cross-scope child, a cycle, a duplicate owner, or a stale child revision must be rejected. The same applies when depth or fan-out exceeds its limits. See workspace composition for the presentation side and permissions for the authority boundary.

What can go wrong