Service names beginning with $ are framework-provided injection tokens. Read
the service guides first if services or
dependency injection are new to you.
This is the multi-page printable view of this section. Click here to print.
Service reference
- 1: $anchorScroll
- 2: $compile
- 3: $controller
- 4: $cookie
- 5: $eventBus
- 6: $exceptionHandler
- 7: $http
- 8: $interpolate
- 9: $location
- 10: $log
- 11: $machine
- 12: $parse
- 13: $rest
- 14: $rootElement
- 15: $rootScope
- 16: $security
- 17: $serviceWorker
- 18: $templateCache
- 19: $templateRequest
- 20: $url
- 21: Binding policies
- 22: Workflow
1 - $anchorScroll
$anchorScroll scrolls to the element identified by a hash or, when omitted,
to the current value of
$location.getHash(),
according to the rules specified in the
HTML spec.
It also watches the URL hash and automatically scrolls to any matched anchor whenever it is changed by $location.
Exact signatures live in TypeDoc:
Configure
Use module.config({ $anchorScroll: ... }) for application-wide scroll policy.
angular.createModule("app", []).config({
$anchorScroll: {
autoScrolling: false,
},
});
Executable sample:
anchor-scroll.html
Manual Scrolling
$location.setHash("details");
$anchorScroll("details");
When automatic scrolling is disabled, call $anchorScroll(...) directly after
coordinating routing, animation, or focus.
Vertical Offset
Use the yOffset property to specify a fixed or dynamic vertical scroll offset.
This is useful when the application has sticky headers.
Example
<style>
.demo2 {
height: 100px;
overflow: auto;
}
.anchor {
border: 2px dashed DarkOrchid;
padding: 10px 10px 400px 10px;
}
.fixed-header {
background-color: rgba(0, 0, 0, 0.2);
height: 50px;
}
.fixed-header > a {
display: inline-block;
margin: 5px 15px;
}
</style>
<section id="demo2" class="demo2">
<div ng-controller="headerCtrl" class="fixed-header">
<button class="btn" ng-click="gotoAnchor(x)" ng-repeat="x in [1,2,3,4,5]">
Go to anchor {{x}}
</button>
</div>
<div id="anchor{{x}}" class="anchor" ng-repeat="x in [1,2,3,4,5]">
Anchor {{x}} of 5
</div>
</section>
window.angular
.createModule('demo2', [])
.run([
'$anchorScroll',
function ($anchorScroll) {
$anchorScroll.yOffset = 100; // always scroll by 50 extra pixels
},
])
.controller('headerCtrl', [
'$anchorScroll',
'$location',
'$scope',
function ($anchorScroll, $location, $scope) {
$scope.gotoAnchor = function (x) {
window.$locationTest = $location;
const newHash = 'anchor' + x;
if ($location.getHash() !== newHash) {
// Pass `newHash` to setHash() and
// $anchorScroll will automatically scroll to it
$location.setHash('anchor' + x);
} else {
// call $anchorScroll() explicitly,
// since getHash() still returns the same value
$anchorScroll();
}
};
},
]);
window.angular.bootstrap(document.getElementById('demo2'), ['demo2']);
For detailed method description, see $anchorScroll.
2 - $compile
3 - $controller
4 - $cookie
Description
The $cookie service offers a simple API for interacting with browser
cookies in AngularTS applications. It allows you to:
- Read existing cookies
- Write new cookies
- Delete cookies
- Store and retrieve JavaScript objects (via JSON serialization)
- Automatically URI-encode and decode values
Example
angular.getModule('app').controller(
'UserCtrl',
/** @param {ng.CookieService} $cookie */
["$cookie", function ($cookie) {
// Write cookie
$cookie.put('session_id', 'abc123');
// Read cookie
const session = $cookie.get('session_id');
// Store object
$cookie.putObject('profile', { name: 'Alice', admin: true });
// Read object
const profile = $cookie.getObject('profile');
// Remove cookie
$cookie.remove('session_id');
}],
);
Cookie behavior can be customized globally with
module.config({ $cookie: ... }).
angular.createModule("app", []).config({
$cookie: {
defaults: {
path: "/",
secure: true,
samesite: "Lax",
},
},
});
Defaults are merged into $cookie.put(), $cookie.putObject(), and
$cookie.remove() calls. Per-call options still override configured defaults.
Cookies are limited to roughly 4 KB, including key and value, so avoid storing large objects; prefer identifiers or short tokens.
Executable sample:
log-cookie.html
For detailed method description, see CookieService
5 - $eventBus
Description
A messaging service, backed by an instance of
EventBus. This implementation is based on
the original
Google Closure EventBus
but uses
queueMicrotask
instead of
Window.setTimeout()
for its async implementation.
$eventBus allows communication between an
Angular application instance and the outside context, which can be a different
module, a non-Angular application, a third-party party library, or even a WASM
application. Additionally, $eventBus can be used to communicate directly with
a template, using ng-channel directive.
$eventBus should not be used for communicating between Angular’s own
primitives.
- For sharing application state: use custom Services and Factories that encapsulate your business logic and manage your model.
- For intercomponent communication: use
$scope.on(),$scope.emit(), and$scope.broadcast()methods.
The example below demonstrates communication between the global window context
and a controller. Note: Ensure topic clean-up after the $scope is
destroyed
Example
<section ng-app="demo">
<div ng-controller="DemoCtrl as $ctrl">
Milliseconds elapsed since the epoch <b>{{ $ctrl.ms }}</b>
</div>
</section>
<!--We are using a regular onclick and `angular` global -->
<button
class="btn btn-dark"
onclick="angular.eventBus.publish('demo', Date.now())"
>
Publish time
</button>
window.angular.createModule('demo', []).controller(
'DemoCtrl',
class {
static $inject = ['$eventBus', '$scope'];
constructor($eventBus, $scope) {
const unsubscribe = $eventBus.subscribe('demo', (val) => {
// `this.ms = val` will not work because `this` is not a proxy
// to trigger change detection, we access controller as a scope property
$scope.$ctrl.ms = val;
});
$scope.on('$destroy', unsubscribe);
}
},
);
Demo
For detailed method description, see EventBus
6 - $exceptionHandler
Description
$exceptionHandler is the central hook for exceptions that escape detached
AngularTS-managed work. This includes DOM and transport events, scheduled tasks,
subscriptions, expression evaluation, and lifecycle callbacks whose original
caller can no longer receive a thrown value or rejected promise.
The decision is: can the initiating caller still receive the failure? If yes,
throw synchronously or reject the returned promise. Use $exceptionHandler only
after that caller boundary no longer exists.
Synchronous API validation throws directly. Promise-returning operations reject their returned promise. Expected transport, workflow, routing, and domain failures remain in their typed operation channels.
By default, it rethrows exceptions that occur during AngularTS-managed execution. This fail-fast behavior ensures errors are visible immediately in development and in unit tests.
IMPORTANT: In AngularJS, $exceptionHandler only caught errors in
expressions and logged them to the console, using a type signature of
$exceptionHandler(exception, [cause]).
AngularTS treats $exceptionHandler as a single error sink and fails eagerly.
It preserves the thrown value unchanged. Applications should throw
Error
objects with a cause when reporting requires structured context.
For type description, see ng.ExceptionHandler.
Configure
Use module.config({ $exceptionHandler: ... }) for application-wide error
reporting policy.
angular.createModule('app', []).config({
$exceptionHandler: {
handler(error) {
myReporter.capture(error);
throw error;
},
},
});
Custom handlers should rethrow after reporting. AngularTS treats
$exceptionHandler as fail-fast, and swallowing errors can hide broken
application state.
Executable sample:
exception-handler.html
7 - $http
The $http service is AngularTS’s built-in HTTP client. Use this page for the
behavioral model and common examples. For exact method signatures, parameters,
return types, and interfaces, use the generated TypeDoc reference:
Usage
Call $http with a full request configuration when you need explicit control:
$http<User>({
method: 'GET',
url: '/api/users/42',
}).then((response) => {
user = response.data;
});
For common HTTP verbs, use shorthand methods:
$http.get<User>('/api/users/42').then(({ data }) => {
user = data;
});
$http.post<User>('/api/users', {
name: 'Ada',
email: 'ada@example.com',
});
Request Behavior
Plain objects are serialized as JSON by the default request transform. File,
Blob, and FormData values are sent as-is so the browser can set the correct
transport headers.
Query parameters are serialized by $httpParamSerializer. Arrays produce
repeated keys, objects are JSON encoded, and null, undefined, and functions
are omitted.
$http.get<Article[]>('/api/articles', {
params: {
category: 'news',
tags: ['featured', 'breaking'],
},
});
Response Behavior
Successful 2xx responses resolve with an HttpResponse<T>. Non-2xx responses,
timeouts, aborts, and network errors reject with the same response shape, so
error handlers can inspect status, statusText, headers, config, and
xhrStatus.
$http.get<User>('/api/users/99').catch((error) => {
if (error.xhrStatus === 'timeout') {
message = 'Request timed out.';
} else if (error.status === 404) {
message = 'User not found.';
}
});
Defaults
Configure defaults before bootstrap with module.config({ $http: ... }), or
mutate runtime defaults through $http.defaults.
angular.createModule("app", []).config({
$http: {
defaults: {
headers: {
common: {
Authorization: "Bearer token",
},
},
withCredentials: true,
},
},
});
Default response transforms strip AngularJS JSON protection prefixes and parse JSON responses automatically.
Executable sample:
http.html
Interceptors
Interceptors are registered with module.config({ $http: ... }). Request hooks
run in registration order; response hooks run in reverse order.
angular.createModule("app", []).config({
$http: {
interceptors: [
() => ({
request(config) {
config.headers.Authorization = `Bearer ${localStorage.getItem("token")}`;
return config;
},
responseError(rejection) {
if (rejection.status === 401) {
window.location.href = "/login";
}
return Promise.reject(rejection);
},
}),
],
},
});
XSRF
$http reads the configured XSRF cookie and sends it in the configured XSRF
header for trusted origins. Add cross-origin APIs explicitly:
angular.createModule("app", []).config({
$http: {
xsrfTrustedOrigins: ["https://api.example.com"],
},
});
Related
8 - $interpolate
9 - $location
$location parses the browser URL into path, search, hash, and state pieces.
Changes made through $location update the browser address bar, and browser
navigation updates the service.
Exact signatures live in TypeDoc:
Read The Current URL
$location.getPath();
$location.getSearch();
$location.getHash();
$location.getUrl();
$location.absUrl();
Update The URL
$location
.setPath("/settings")
.setSearch({ tab: "profile" })
.setHash("details");
Setter methods return $location, so updates can be chained.
Use setSearchParam() when only one query parameter should change. Passing
null or undefined removes that parameter.
$location.setSearchParam("page", 2);
$location.setSearchParam("filter", null);
Navigation Events
$locationChangeStart is broadcast on $rootScope before the URL changes. Call
event.preventDefault() from a listener to cancel the navigation.
$locationChangeSuccess is broadcast after the URL changes. In HTML5 mode,
listeners may also receive the new and old history state values when the browser
supports the History API.
$rootScope.on("$locationChangeStart", (event, newUrl, oldUrl) => {
if (shouldBlock(newUrl, oldUrl)) {
event.preventDefault();
}
});
$rootScope.on("$locationChangeSuccess", (_event, newUrl) => {
analytics.track("page_view", { url: newUrl });
});
Configure
Use module.config({ $location: ... }) for application-wide URL policy.
angular.createModule("app", []).config({
$location: {
html5Mode: {
enabled: true,
requireBase: false,
rewriteLinks: true,
},
hashPrefix: "!",
},
});
html5Mode may also be a boolean when only the enabled flag should change:
angular.createModule("app", []).config({
$location: {
html5Mode: false,
},
});
Executable sample:
location.html
10 - $log
Using $log
Default implementation of LogService safely writes the message into the browser’s console (if present). The main purpose of this service is to simplify debugging and troubleshooting, while allowing the developer to modify the defaults for live environments.
Example*
angular.getModule('demo').controller('MyController', ["$log", ($log) => {
$log.log('log');
$log.info('info');
$log.warn('warn!');
$log.error('error');
$log.debug('debug');
}]);
To reveal the location of the calls to $log in the JavaScript console, you can
“blackbox” the AngularTS source in your browser:
Note: Not all browsers support blackboxing.
The default is to suppressdebugmessages unless logging config enables them.
Configure
Use module.config({ $log: ... }) for application-wide logging policy.
angular.createModule('demo', []).config({
$log: {
debug: true,
},
});
debug controls whether $log.debug(...) writes to the console. The default is
false.
Beacon delivery
Selected log levels can also be delivered through the browser’s Beacon API.
Beacon delivery is disabled unless an endpoint is configured and defaults to the
error level when levels is omitted.
angular.createModule('demo', []).config({
$log: {
beacon: {
url: '/api/client-logs',
levels: ['warn', 'error'],
},
},
});
The default serializer sends an application/json Blob containing
timestamp, level, and args. Applications that need redaction or additional
metadata can register and select a serializer:
angular
.getModule('demo')
.factory(
'clientLogSerializer',
() => (entry) =>
JSON.stringify({
timestamp: entry.timestamp,
level: entry.level,
message: String(entry.args[0]),
}),
)
.config({
$log: {
beacon: {
url: '/api/client-logs',
serializer: 'clientLogSerializer',
},
},
});
Beacon queueing has no response callback or delivery acknowledgement. AngularTS
does not retry or persist rejected entries. Failures produce a local warning by
default; set failure: "ignore" to suppress it.
Beacon delivery automatically participates in $security; there is no separate
logging security switch. AngularTS checks the endpoint and configured request
credentials before serializing an entry. A denied remote delivery never
suppresses the local log call, and repeated policy denials produce at most one
local warning.
Beacon requests cannot carry application-defined Authorization headers, so
Bearer and Basic credential policies deny Beacon delivery. Use a cookie endpoint
for authenticated Beacon logging. If the endpoint requires custom headers, use
$http or fetch() with keepalive: true instead. Redaction and payload
shaping remain the responsibility of the configured serializer.
You can also replace the logger implementation:
angular.createModule('demo', []).config({
$log: {
logger: () => ({
log: console.log,
info: console.info,
warn: console.warn,
error: console.error,
debug: console.debug,
}),
},
});
Executable sample: log-cookie.html
Decorating $log
You can also override any $log service method with module.decorator(). Below
is a simple example that overrides default $log.error to log errors to both
console and a backend endpoint.
Example*
angular
.getModule('demo')
.decorator('$log', ["$delegate", "$http", "$exceptionHandler", ($delegate, $http, $exceptionHandler) => {
const originalError = $delegate.error;
$delegate.error = () => {
originalError.apply($delegate, arguments);
const errorMessage = Array.prototype.slice.call(arguments).join(' ');
$http.post('/api/log/error', { message: errorMessage });
};
return $delegate;
}]);
Overriding console
If your application is already heavily reliant on default
console logging or
you are using a third-party library where logging cannot be overriden, you can
still take advantage of Angular’s services by modifying the globals at runtime.
Below is an example that overrides default console.error to logs errors to
both console and a backend endpoint.
Example*
angular.getModule('demo').run(["$http", ($http) => {
/**
* Decorate console.error to log error messages to the server.
*/
const $delegate = window.console.error;
window.console.error = (...args) => {
try {
const errorMessage = args
.map((arg) => (arg instanceof Error ? arg.stack : JSON.stringify(arg)))
.join(' ');
$delegate.apply(console, args);
$http.post('/api/log/error', { message: errorMessage });
} catch (e) {
console.warn(e);
}
};
/**
* Detect errors thrown outside Angular and report them to the server.
*/
window.addEventListener('error', (e) => {
window.console.error(e.error || e.message, e);
});
/**
* Optionally: Capture unhandled promise rejections
*/
window.addEventListener('unhandledrejection', (e) => {
window.console.error(e.reason || e);
});
}]);
Combining both $log decoration and console.log overriding provides a robust
and flexible error reporting strategy that can adapt to your use-case.
* array notation and HTTP error handling omitted for brevity
11 - $machine
$machine
$machine creates a small reactive state machine for UI and application flows.
It is more than a regular service or factory because the returned machine is
designed to be wrapped by AngularTS scope proxies: state and data update
templates naturally, and send() batches transition work when a scope proxy is
observing the machine.
Use ordinary services for IO, persistence, and shared domain logic. Use
$machine when the important part is the allowed flow between states.
Use $workflow when the flow also
needs command boundaries, diagnostics, snapshots, restore, retry, or repeat.
Machine naming, inference, and public-type placement follow the shared API ergonomics contract.
Create a Machine
The smallest complete game needs only a state tree and a score. Assign the
machine to a controller property and let the template call send() directly:
const game = $machine({
initial: 'ready',
data: { score: 0 },
states: {
ready: { on: { start: { to: 'playing' } } },
playing: { on: { tap: { to: 'won' } } },
won: { on: { restart: { to: 'playing' } } },
},
});
<section ng-controller="TapGameCtrl as $ctrl">
<p>{{$ctrl.game.state}}: {{$ctrl.game.data.score}} / 3</p>
<button
ng-if="$ctrl.game.matches('ready')"
ng-click="$ctrl.game.send('start')"
>
Start
</button>
<button
ng-if="$ctrl.game.matches('playing')"
ng-click="$ctrl.game.send('tap')"
>
Tap
</button>
<button
ng-if="$ctrl.game.matches('won')"
ng-click="$ctrl.game.send('restart')"
>
Play again
</button>
</section>
state identifies the machine’s active state. State-tree transitions move to
the explicit to state, while update mutates reactive game data. A
to(context) resolver handles transitions whose target depends on runtime data.
Ownership Patterns
Create machines with $machine(config) and assign the returned object to the
owner that should keep the flow alive.
Controller-owned machines are enough for local UI flow data:
app.controller('SessionCtrl', [
'$machine',
function ($machine) {
this.session = $machine({
initial: 'setup',
data: { roomId: '' },
states: {
setup: {
on: {
join: {
to: 'waiting',
update({ data, payload }) {
data.roomId = payload.roomId;
},
},
},
},
waiting: {},
},
});
},
]);
AppContext models can own machines that must survive DOM root destruction:
app.model('sessionRuntime', [
'$machine',
function ($machine) {
return {
session: $machine({
initial: 'setup',
data: { roomId: '' },
states: {
setup: {
on: {
join: {
to: 'waiting',
update({ data, payload }) {
data.roomId = payload.roomId;
},
},
},
},
waiting: {},
},
}),
};
},
]);
Runtime adapters and game loops can keep the same machine outside the DOM:
app.controller('GameCtrl', [
'$machine',
function ($machine) {
const game = $machine({
initial: 'playing',
data: { frame: 0 },
states: {
playing: {
on: {
tick: {
update({ data }) {
data.frame += 1;
},
},
},
},
},
});
this.game = game;
requestAnimationFrame(function tick() {
game.send('tick');
requestAnimationFrame(tick);
});
},
]);
Workflows can own a machine when command execution needs a local flow gate:
app.factory('sessionWorkflowMachine', [
'$machine',
function ($machine) {
return $machine({
initial: 'idle',
data: { token: '' },
states: {
idle: {
on: {
start: {
to: 'authenticating',
},
},
},
authenticating: {
on: {
authenticated: {
to: 'ready',
update({ data, payload }) {
data.token = payload;
},
},
},
},
ready: {},
},
});
},
]);
Register a Named Machine
Use module.machine(name, config) when a machine should be injectable by name:
app.machine('sessionMachine', {
initial: 'setup',
data: {
roomId: '',
},
states: {
setup: {
on: {
join: {
to: 'waiting',
update({ data, payload }) {
data.roomId = payload.roomId;
},
},
},
},
waiting: {},
},
});
app.controller('SessionCtrl', [
'sessionMachine',
function (sessionMachine) {
this.session = sessionMachine;
},
]);
You can also provide a resolvable config factory so it can read injectables at registration time:
function buildSessionMachine(appSettings) {
return {
initial: appSettings.initialMode,
data: {
roomId: '',
error: '',
},
states: {
setup: {
on: {
join: {
to: 'waiting',
update({ data, payload }) {
data.roomId = payload.roomId;
},
},
},
},
waiting: {},
},
};
}
buildSessionMachine.$inject = ['appSettings'];
app.machine('sessionMachine', buildSessionMachine);
This is useful when defaults depend on environment, user session data, or persisted configuration.
Named machines are regular DI services. The injector creates one machine instance and reuses it for that injectable name.
Template API
The machine exposes a small template-friendly API:
<button ng-disabled="!$ctrl.session.matches('setup')">Random</button>
<div ng-show="$ctrl.session.matches('waiting')">Waiting for opponent...</div>
<span>{{ $ctrl.session.data.roomId }}</span>
Runtime API
session.state; // current state string
session.data; // reactive data object
session.matches('setup'); // true when current state is setup
session.can('join', payload); // true when current state can run join
session.send('join', payload); // structured transition result
session.snapshot(); // structured clone of { state, data }
session.restore(snapshot);
send(type, payload) returns a structured result with ok, status, type,
from, to, and an optional denial reason. ok is true when a transition
entry exists for the current state, its optional guard passes, and its update
hook runs when provided. Missing transitions and blocked guarded transitions
return ok: false with a specific status and do not throw. Use $workflow when
failures need diagnostics, retry, or recovery.
ok is the broad branch: successful results have status transitioned or
updated; failed results have a denial, invalid-event, or missing-transition
status. Machine results intentionally do not carry diagnostics. The status is
the decision, while workflows own durable failure evidence.
State-tree transition shape:
tonames the next state or resolves it from readonly transition input- omit
tofor a same-state data update update({ data, payload })mutates reactive dataguard({ data, payload })blocks the transition when it returnsfalsedenied({ data, payload })may record local denial state
Routing is explicit. Static to is the normal path. Use a synchronous,
side-effect-free to(context) resolver when the next state depends on runtime
data, such as a game move ending in win, draw, or continued play. AngularTS
resolves one target before guard and policy evaluation; hooks and update()
receive that same readonly target. Return values from update() are ignored.
can(type, payload) checks the transition table, target resolver, policy, and
any optional guard for the current state. It does not run update or denied.
Target resolvers and guards should be side-effect-free because templates may
call can() repeatedly.
Status values are transitioned, updated, missing-transition,
guard-denied, policy-denied, and invalid-event. When policy denies a
transition, the result includes the policy decision’s optional reason.
Denial paths are narrow. guard-denied only means the transition guard returned
false; transition-local denied() runs only for that guard-denial case.
Missing transitions and invalid event names do not run before, update,
after, denied, enter hooks, exit hooks, or global transition hooks.
policy-denied is reserved for framework policy gates. It does not run
denied, mutation hooks, state hooks, global transition hooks, or legacy
transition targets.
Transition Policy
Use policy when the application has a cross-cutting synchronous gate that
should apply to every transition in a machine:
const session = $machine({
id: 'session',
initial: 'setup',
data: { roomId: '' },
states: {
setup: {
on: {
join: {
to: 'waiting',
update({ data, payload }) {
data.roomId = payload.roomId;
},
},
},
},
waiting: {},
},
policy(context) {
if (context.type === 'join' && !context.payload.roomId) {
return { type: 'deny', reason: 'room_required' };
}
return 'allow';
},
});
session.send('join', { roomId: '' }).status; // "policy-denied"
Policy contexts include operation: "machine.transition", machineId, type,
from, to, event-narrowed payload, readonly data, an observational
machine view, and optional meta. The policy and guard machine view exposes
state, data, can(), matches(), and snapshot() but cannot dispatch or
restore live state. Policies must be synchronous so can() and send() stay
synchronous. Put async authorization, retries, persistence, and recovery in
$workflow or another service and then project the result back through a
machine event.
State-tree transitions with static to give policy the strongest pre-execution
context. Legacy callback transitions do not run their target just to discover a
future state, so policy sees to as the current state there.
Guarded Transitions
Use guard when a transition has a cheap synchronous condition that templates
or controllers should be able to ask about through can():
const session = $machine({
initial: 'setup',
data: {
roomId: '',
inviteCode: '',
},
states: {
setup: {
on: {
join: {
to: 'waiting',
guard({ data, payload }) {
return !!data.inviteCode && payload.roomId !== '';
},
update({ data, payload }) {
data.roomId = payload.roomId;
},
denied({ data }) {
data.roomId = '';
},
},
},
},
waiting: {},
},
});
session.can('join', { roomId: 'abc' }); // evaluates guard only
session.send('join', { roomId: 'abc' }); // runs update when guard passes
Guards and updates are synchronous. Put async work, retries, diagnostics, and
command recovery in $workflow, then
use a machine transition to project the resulting state/data into the UI.
Example: Session Wizard
A machine is also useful for local wizard flow where the UI should only expose legal next actions:
const wizard = $machine({
initial: 'profile',
data: { name: '' },
states: {
profile: {
on: {
next: {
to: 'complete',
guard: ({ data }) => data.name.trim() !== '',
},
},
},
complete: {},
},
});
Static to keeps the flow inspectable: next, back, and submit declare
their route before mutation code runs. The same-state accept event omits to
because it only updates wizard data.
Example: Tic Tac Toe
A machine is useful when user actions must follow a small set of legal states.
This example keeps a tic tac toe game in playing until a move produces a win
or draw. Once the machine reaches xWon, oWon, or draw, there is no move
transition for that state, so further moves return false.
const game = $machine({
initial: 'playing',
data: {board: Array(9).fill('-'), nextPlayer: 'X'},
states: {
playing: {on: {move: {
guard: ({data, payload}) => data.board[payload.index] === '-',
update({data, payload}) {
data.board[payload.index] = data.nextPlayer;
data.nextPlayer = data.nextPlayer === 'X' ? 'O' : 'X';
},
to: ({data, from}) => winnerOf(data.board) || from,
}}},
xWon: {},
oWon: {},
draw: {},
},
});
},
});
}]);
<button
ng-repeat="cell in $ctrl.game.data.board"
ng-disabled="!$ctrl.game.matches('playing') || cell !== '-'"
ng-click="$ctrl.game.send('move', { index: $index })"
>
{{ cell }}
</button>
<p ng-show="$ctrl.game.matches('playing')">
Next: {{ $ctrl.game.data.nextPlayer }}
</p>
<p ng-show="$ctrl.game.data.winner">Winner: {{ $ctrl.game.data.winner }}</p>
The transition owns both validation and projection. Its to resolver predicts
the next state without mutation; a valid move then updates data.board and
whose turn is next. An invalid move records lastError in denied, leaving
state unchanged. Machine transitions do not roll back data mutations when
hooks throw.
Persist With Transition Hooks
Transition hooks are a convenient place to persist the game after each handled
move. The hook runs after state has been updated, so the snapshot contains the
terminal state when the move ends the game.
const persistedGame = $machine({
...gameConfig,
hooks: {
transition: ({ machine }) =>
localStorage.setItem('game', JSON.stringify(machine.snapshot())),
},
});
const saved = localStorage.getItem('game');
if (saved) persistedGame.restore(JSON.parse(saved));
restore() does not run hooks, so loading the saved game will not immediately
write another snapshot. The next successful send() call will persist the
updated state and data.
Snapshots
Use snapshot() to capture durable state and data. A snapshot is a structured
clone of { state, data }:
const snapshot = session.snapshot();
localStorage.setItem('session', JSON.stringify(snapshot));
A snapshot contains only state and data. It has no version or machine id,
and it does not include transitions or hooks; those still come from the machine
config. Use $workflow or a workflow supervisor when persisted recovery needs
versioning, diagnostics, history, retry, or migration.
The clone uses the browser’s structuredClone() behavior. Values such as
objects, arrays, Dates, Maps, Sets, typed arrays, and cycles can be cloned.
Functions, DOM nodes, and other non-cloneable values will throw the native
structured clone error.
JSON persistence is a narrower contract. Use JSON.stringify(snapshot) only
when data is JSON-compatible, or add your own encode/decode layer before
storing the snapshot.
Use restore(snapshot) to recover an existing machine:
const snapshot = JSON.parse(localStorage.getItem('session'));
session.restore(snapshot);
restore() mutates the existing data object in place so scope proxies and
templates keep their identity. Keys missing from the snapshot are removed,
nested plain objects are merged in place, and non-plain values like arrays are
replaced with cloned snapshot values. Restore accepts persisted input as
unknown, validates its shape, and rejects a state that is not declared in
states. Restore does not run transition hooks.
The configured initial state and every resolved transition target must also
exist in states. Invalid built-in machine states fail at their boundary
instead of leaving the machine in an unreachable state.
Hooks
Use hooks.enter and hooks.exit for state-specific effects. Use
hooks.transition for logging or cross-cutting work that should run after any
handled transition, including same-state transitions:
const session = $machine({
initial: 'setup',
data: { status: 'idle' },
states: {
setup: { on: { join: { to: 'waiting' } } },
waiting: { on: { matched: { to: 'playing' } } },
playing: {},
},
hooks: {
exit: { setup: ({ data }) => (data.status = 'joining') },
enter: { waiting: ({ data }) => (data.status = 'waiting') },
transition: ({ type, from, to }) => console.log(type, from, to),
},
});
Hook context contains type, from, to, payload, data, and machine.
exit runs before state changes, enter runs after state changes, and
transition runs after state-specific hooks. Missing transitions do not run
hooks.
Hooks run synchronously inside the same send() batch. A hook may call
machine.send() to run another valid transition; nested sends are batched with
the outer transition. If a transition or hook throws, the error is rethrown,
prior data/state mutations are kept, and AngularTS still schedules bound scopes
for the keys touched by the attempted transition.
Scope Ownership
$machine(config) does not tie the machine to the lifetime of any one scope.
The machine stores durable state and data internally. When an AngularTS scope
proxy wraps the machine, the machine registers with that proxy and uses its
scheduler for reactive template updates and batched send() calls.
That means a machine can be created once, assigned to a component, survive that component being destroyed, and later be assigned to another scope.
Prefer $machine(config) plus assignment to the controller, model, service,
workflow, or runtime adapter that owns the machine. Named machines registered
with module.machine() follow the same rule: they bind when a scope proxy
observes them.
TypeScript
type SessionData = {
roomId: string;
error: string;
};
const session = $machine({
initial: 'setup',
data: {
roomId: '',
error: '',
} satisfies SessionData,
states: {
setup: {
on: {
join: {
to: 'waiting',
update({ data }) {
data.roomId = 'abc';
},
},
},
},
waiting: {},
},
});
session.send('join', { roomId: 'abc' });
session.send('reset');
Machine definitions are usable without generic parameters. Event names are
inferred from the on maps and state names are inferred from states. Payloads
remain unknown until an explicit event map supplies their types.
Add an event map when you want strict checked event names and payloads:
type SessionEvents = {
join: { roomId: string };
reset: undefined;
};
const session = $machine<SessionData, SessionEvents>({
initial: 'setup',
data: { roomId: '', error: '' },
states: {
setup: {
on: {
join: {
to: 'waiting',
update: ({ data, payload }) => (data.roomId = payload.roomId),
},
},
},
waiting: { on: { reset: { to: 'setup' } } },
},
});
The ordinary authoring path does not require extraction helpers. Package authors
can import MachineDataOf, MachineEventsOf, MachineEventNamesOf, and
MachineStatesOf directly from @angular-wave/angular.ts/services/machine when
an adapter needs to derive types from a reusable definition.
12 - $parse
13 - $rest
The $rest service creates typed resource clients on top of a REST backend. Use
this page for the usage model and examples. Exact class members, method
signatures, return types, and configuration interfaces live in TypeDoc:
Creating A Resource
Inject $rest and call it with a base URL:
const posts = $rest<Post, number>('/api/posts');
The returned RestService supports
common CRUD workflows:
const all = await posts.list();
const one = await posts.get(42);
const created = await posts.create({ title: 'Hello' } as Post);
const updated = await posts.update(42, { title: 'Updated' });
await posts.delete(42);
get(), create(), and update() return null when the backend returns no
entity. delete() resolves with no value. All backend failures reject instead
of being converted into fallback values.
Entity Mapping
Pass an entity class when raw JSON should be converted into richer objects.
class Post {
id!: number;
title!: string;
createdAt: Date;
constructor(raw: any) {
Object.assign(this, raw);
this.createdAt = new Date(raw.created_at);
}
}
const posts = $rest<Post, number>('/api/posts', Post);
const post = await posts.get(1);
When an entity class is supplied, response objects are passed through
new entityClass(data) before they are returned.
Request Options
The third factory argument is merged into backend requests. With the default HTTP backend, use it for headers, credentials, cache options, transforms, or custom param serialization.
const posts = $rest<Post, number>('/api/posts', Post, {
headers: { 'X-Tenant': tenantId },
withCredentials: true,
});
Pass backend when a resource should use a custom backend. The backend receives
a normalized RestRequest with
expanded URLs, params, request data, collection URL, and resource id. This is
the extension point for tests, local persistence, IndexedDB, the browser Cache
API, or composed network/cache behavior.
For cached reads, wrap the HTTP backend with
CachedRestBackend:
const posts = $rest<Post, number>('/api/posts', Post, {
backend: new CachedRestBackend({
network: new HttpRestBackend($http),
cache: appCache,
strategy: 'network-first',
}),
});
createRestCacheKey() is used internally by
CachedRestBackend; it is
not part of the top-level AngularTS namespace. Cache stores receive the final
key string through the
RestCacheStore methods and
should treat it as opaque.
Request Bodies
When the default HTTP backend is used, request data is serialized by $http.
Scope proxies are deproxied before JSON serialization, so proxy helper
properties such as _target, _handler, and _proxy are not sent to the
server. AngularTS-generated repeat identity is stored in internal metadata, not
on the object, so it is not included in write payloads either.
Explicit application-owned fields are still normal data. If your model defines a
property such as $hashKey, $rest does not remove it.
URI Templates
Resource URLs support RFC 6570 URI templates. Variables wrapped in braces are
expanded from the params passed to methods such as list().
const repos = $rest<Repo>('/api/{org}/repos');
const angularRepos = await repos.list({
org: 'angular-wave',
type: 'public',
});
The org value expands into the path. Remaining params are forwarded to $http
as query parameters.
Shared Defaults
Configure shared request defaults before bootstrap when a group of resources should use the same backend behavior.
angular.createModule('app', []).config({
$rest: {
defaults: {
withCredentials: true,
timeout: 5000,
},
},
});
Demo
The CRUD demo at /src/services/rest/rest-crud-demo.html talks to the Go demo
backend through /api/tasks. It uses ng-repeat for rows, $rest for CRUD
operations, and a cache strategy toggle for network-first, cache-first, and
stale-while-revalidate.
Related
14 - $rootElement
15 - $rootScope
16 - $security
$security centralizes framework security policy decisions so apps can keep
cross-cutting concerns out of every service call. The runtime-composed service
allows by default and applies typed configuration from
app.config({ $security: ... }).
Services consume this policy before sensitive side effects:
$httpcallscheck()once and applies credentials from its allow decision.$logsynchronously gates Beacon delivery before serializing an entry.- Router transitions call
check()before state changes proceed.
When no fallback is configured, the fallback decision is allow.
Exact policy signatures live in TypeDoc:
Configure
Use app.config({ $security: ... }) to set your app policy:
angular.createModule('app', []).config({
$security: {
fallback: 'deny',
allowInsecureOrigins: ['http://localhost:3000'],
isAuthenticated: function isAuthenticated() {
return sessionStore.currentUser != null;
},
credentials: {
bearer: function getToken() {
return sessionStorage.getItem('token');
},
cookie: true,
order: ['bearer', 'cookie'],
},
permissions: async function permissions(permission, context) {
return permissionStore.allows(permission, context.to.name);
},
},
});
fallback controls unspecified behavior ("allow" | "deny"). Redirects are
declared on route policies where a target is required.
Configuring a credential enables its request transport scheme. It does not
declare that the current user is authenticated. isAuthenticated supplies that
application state as a boolean or contextual async function. The optional
credentials.order changes the default bearer, basic, then cookie precedence
without requiring a second list of enabled schemes.
allowInsecureOrigins is an explicit allowlist for HTTP origins that may carry
sensitive credentials. All other insecure origins remain blocked.
Malformed origin strings are ignored, preserving fail-closed credential
transport. Invalid policy enums and configuration shapes fail during setup.
credentials.bearer attaches a bearer token to allowed $http requests.
Caller-provided Authorization headers are recognized automatically.
credentials.basic accepts username and password value sources and encodes
the resulting credentials for the Authorization header.
credentials.cookie tells request services to use browser-managed cookies by
enabling credentialed requests instead of manually writing a Cookie header.
Request policy contexts identify their framework transport. HTTP, Beacon, Worker, and Service Worker creation all pass through the same security gate. Beacon participates automatically, but its Web Platform API cannot apply custom headers. Bearer and Basic policies therefore deny Beacon delivery, while an enabled cookie session permits it. Security evaluation remains synchronous inside the Beacon transport so lifecycle delivery is never delayed by a Promise.
permissions receives each required permission and the current navigation
context. It may return a boolean or a Promise. A static permission array is a
shorthand for applications that do not need contextual authorization.
Route Policy
States can declare inherited navigation policy. The router merges policy from
the target state path and passes the effective policy to $security before
resolves, controllers, or views run.
angular
.createModule('admin', [])
.router({
name: 'admin',
url: '/admin',
abstract: true,
template: '<admin-layout ng-view></admin-layout>',
policy: {
navigation: {
authenticated: true,
permissions: ['admin:read'],
redirectTo: 'login',
},
},
})
.router({
name: 'admin.users',
url: '/users',
component: 'adminUsers',
});
authenticated: true is satisfied only when the configured isAuthenticated
evaluator resolves true. Credential transport and authentication state remain
separate.
Required permissions are resolved by the configured permissions evaluator. If
a required permission is missing, navigation is denied or redirected to the
route policy’s redirectTo target.
Non-security route policies (loading, error, fallbackTo, retry) run in
the same transition pipeline but do not replace authentication/authorization.
They are complementary: $security decides whether navigation is allowed for
the current principal, and route policies decide how the transition is rendered
or recovered when that navigation remains allowed.
Use public: true on a child state to explicitly clear inherited navigation
requirements. A public declaration cannot also specify authenticated,
permissions, redirectTo, or reason because those fields would be
contradictory or ignored.
Security policy has no public provider. Use app.config({ $security: ... });
route matching remains exclusively in the router state tree.
Executable config sample: security.html
Executable route policy sample:
policy.html
retry-fallback.html
17 - $serviceWorker
$serviceWorker wraps browser-owned service worker registration, readiness,
update observation, controller changes, and page-to-worker messaging.
Use it when the browser owns the page-control lifecycle. Use $worker for
page-owned background computation. Use $rest and RestBackend for request and
cache composition. Use $workflow for command execution and recovery; a service
worker can be an activity boundary, not the workflow engine.
Exact method signatures live in TypeDoc:
ServiceWorkerServiceServiceWorkerConfigServiceWorkerUpdateStateServiceWorkerPostOptionsServiceWorkerRequestOptions
Registration
Configure automatic registration on the module when the application has one stable worker script:
angular.createModule('app', []).serviceWorker('/sw.js', {
scope: '/',
updateViaCache: 'none',
autoRegister: true,
});
Or configure the script without automatic registration and start it explicitly:
angular.createModule('app', []).serviceWorker('/sw.js', {
scope: '/',
updateViaCache: 'none',
});
$serviceWorker.register();
// Override browser registration options without repeating the configured URL.
$serviceWorker.register({ scope: '/feature/' });
$serviceWorker.ready().then(() => {
status = $serviceWorker.status;
});
The default policy is observation only. AngularTS does not reload the page, activate a waiting worker, or pick a cache strategy for the app.
Destroying an AngularTS runtime removes its service-worker event listeners and
reactive bindings. It does not call unregister(); browser registration remains
active until the application explicitly unregisters it.
registrationState and updateState are readonly framework-owned views. Their
properties update reactively as the browser registration changes.
Executable sample:
service-worker.html
Update Prompt
Observe update state and make activation a user choice:
$serviceWorker.onUpdate((state) => {
if (state.waiting) {
showUpdatePrompt = true;
}
});
If the user opts in, send your own protocol message to the waiting worker. Keep reload separate so the app can save state, finish work, or defer navigation.
function activateUpdate() {
$serviceWorker.registration?.waiting?.postMessage({
type: 'app:skip-waiting',
});
}
$serviceWorker.onControllerChange(() => {
showReloadButton = true;
});
function reloadAfterUserConfirms() {
window.location.reload();
}
Messages
Use $serviceWorker.post(...) for simple page-to-worker messages after a
controller is available:
$serviceWorker.onMessage((event) => {
if (event.data.type === 'app:pong') {
lastWorkerReply = event.data;
}
});
$serviceWorker.post({
type: 'app:ping',
}, {
target: 'controller',
});
For request/response protocols, request() creates a dedicated
MessageChannel, transfers its response port, and applies a timeout:
const profile = await $serviceWorker.request<Profile>(
{ type: 'profile:load' },
{ timeout: 5000 },
);
The service-worker message handler responds through event.ports[0]. AngularTS
does not add request ids or prescribe an application message envelope.
Security
Registration participates in $security automatically. Bearer and Basic
credential policies deny registration because the Web Platform registration API
cannot attach application-defined Authorization headers. Cookie credentials
remain browser-managed.
REST And Cache
Do not make $rest depend on service workers. REST caching belongs behind
RestBackend and RestCacheStore; a service-worker-backed REST cache should be
an explicit backend adapter when the app needs that boundary.
Unsupported Browsers
When navigator.serviceWorker is unavailable, $serviceWorker.supported is
false, status is unsupported, and async lifecycle methods reject with a
stable service-worker error. Applications can render a non-offline experience
without special provider wiring.
Related
18 - $templateCache
$templateCache is a
Map
object used by template loading and component rendering.
Configure the cache before bootstrap with
module.config({ $templateCache: ... }).
angular.createModule("demo", []).config({
$templateCache: {
cache: new Map([["templateId.html", "This is the content of the template"]]),
},
});
Executable sample:
template-cache.html
The first time a template is used, it is loaded in the template cache for quick retrieval. You can load templates directly into the cache in a script tag by using $templateRequest or by consuming the $templateCache service directly.
Adding via the script tag:
Example
<script type="text/ng-template" id="templateId.html">
<p>This is the content of the template</p>
</script>
Note: the script tag containing the template does not need to be included in the
head of the document, but it must be a descendent of the $rootElement (e.g.
element with ng-app attribute), otherwise the template will be ignored.
Adding via the $templateCache service:
Example
const myApp = angular.createModule('myApp', []).run(["$templateCache", ($templateCache) => {
$templateCache.set('templateId.html', 'This is the content of the template');
}]);
To retrieve the template, simply use it in your component:
myApp.component('myComponent', {
templateUrl: 'templateId.html',
});
or include it with ng-include:
<div ng-include="'templateId.html`"></div>
or get it via the $templateCache service:
myApp.controller(
'Test',
["$templateCache", class {
constructor($templateCache) {
const tmp = $templateCache.get('templateId.html');
}
}],
);
19 - $templateRequest
$templateRequest fetches templates through $http and stores successful
responses in $templateCache.
Configure template request defaults before bootstrap with
module.config({ $templateRequest: ... }).
angular.createModule("demo", []).config({
$templateRequest: {
httpOptions: {
headers: {
"X-Template": "configured",
},
withCredentials: true,
},
},
});
Executable sample:
template-request.html
Usage
$templateRequest("./panel.html").then((template) => {
$templateCache.set("panel.html", template);
});
$templateRequest assumes template URLs are trusted by the caller. Security
policy for resource URLs belongs to $compile.resourceUrlPolicy when templates
are loaded through compiler bindings such as ng-include.
20 - $url
21 - Binding policies
Binding policies
AngularTS uses synchronous callbacks configured through $compile to sanitize
or validate values at their DOM destination. The former $sce and
$sceDelegate services and their configuration keys have been removed.
Configuration
Each callback receives a string and must return a string or throw to reject it. Omitted callbacks retain the framework defaults.
| Configuration callback | Destination | Default |
|---|---|---|
htmlPolicy | ng-bind-html and HTML properties | Reject nonempty strings |
urlPolicy | Link URLs | Block dangerous schemes |
mediaUrlPolicy | Media URLs and each srcset candidate | Allow supported media schemes and image data URLs |
resourceUrlPolicy | Templates and embedded resources | Allow same-origin HTTP(S) resources |
scriptPolicy | Executable JavaScript source | Reject nonempty strings |
scriptUrlPolicy | Script-loading URLs | Allow same-origin HTTP(S) resources |
angular.createModule("app", []).config({
$compile: {
htmlPolicy: (html) => DOMPurify.sanitize(html),
resourceUrlPolicy: (value) => {
const url = new URL(value, document.baseURI);
if (url.origin !== location.origin) {
throw new Error("External resource rejected");
}
return value;
},
},
});
Supply your application’s sanitizer implementation, such as DOMPurify, before using this configuration. A policy callback is responsible for the values it approves. Policies are configured before bootstrap and are not injectable services.
Native TrustedHTML, TrustedScript, and TrustedScriptURL values are
supported at their corresponding destinations. The framework does not create
browser Trusted Types policies for the application.
Executable sample: sce.html.
22 - Workflow
Workflow
$workflow runs asynchronous operations through declarative lifecycles. A
definition states where a command may run and what state and data changes occur
while it is pending, after success, and after failure. Application code only
requests the operation.
Use $machine for synchronous event-driven state. Use $workflow when an
operation can wait, fail, time out, be cancelled, or require recovery.
Create A Workflow
app.workflow('checkout', {
initial: 'ready',
data: {
receipt: null,
failure: '',
},
commands: {
submit: {
from: 'ready',
pending: 'processing',
execute: ({ input, signal }) =>
checkoutApi.submit(input, { signal }),
success: {
to: 'completed',
update({ data, output }) {
data.receipt = output;
data.failure = '';
},
},
failure: {
to: 'failed',
update({ data, diagnostic }) {
data.failure = diagnostic.message;
},
},
},
},
});
Inject the named workflow and request the command:
app.controller('CheckoutCtrl', ["checkout", function (checkout) {
this.checkout = checkout;
this.submit = async (order) => {
const result = await checkout.run('submit', order);
if (!result.ok) {
console.error(result.diagnostics);
}
};
}]);
Templates observe workflow state and data directly:
<button
ng-click="$ctrl.submit($ctrl.order)"
ng-disabled="!$ctrl.checkout.can('submit')"
>
Submit
</button>
<p ng-if="$ctrl.checkout.state === 'processing'">Processing</p>
<p ng-if="$ctrl.checkout.state === 'failed'">
{{ $ctrl.checkout.data.failure }}
</p>
Command Lifecycle
Every command declares:
from: state or states from which the command may run.pending: state entered before execution starts.execute: optional synchronous or asynchronous operation.success: state entered when execution returns.failure: state entered when execution throws or rejects.
pending, success, and failure accept either a state name or an object with
to and update:
success: {
to: 'completed',
update({ data, input, output, command }) {
data.receipt = output;
},
}
Lifecycle updates are the writable boundary for workflow data. The data passed
to execute is readonly, including nested plain objects, arrays, maps, and
sets. This prevents a failed command from leaving untracked imperative changes.
Immediate Commands
An operation that only changes workflow state can omit execute:
commands: {
reset: {
from: ['completed', 'failed'],
pending: 'resetting',
success: {
to: 'ready',
update({ data }) {
data.receipt = null;
data.failure = '';
},
},
failure: 'failed',
},
}
await checkout.run('reset');
External approvals, WebSocket messages, user actions, and repair requests enter
a workflow as commands. Workflows do not expose a separate event or send()
API.
Results
run() always resolves to a WorkflowResult:
const result = await checkout.run('submit', order);
if (result.ok) {
console.log(result.output);
} else if (result.status === 'timeout') {
console.warn('The operation timed out.');
} else {
console.warn(result.diagnostics);
}
Successful results have status completed. Failed results use failed,
cancelled, timeout, or rejected.
rejected means execution did not fail unexpectedly. The command was unknown,
was not allowed from the current state, violated its concurrency declaration,
or called reject() with a controlled diagnostic.
Controlled Rejection
Use reject() for an expected business decision:
execute({ input, reject }) {
if (!input.acceptedTerms) {
return reject({
code: 'checkout.termsRequired',
message: 'Terms must be accepted.',
recoverable: true,
});
}
return checkoutApi.submit(input);
}
The framework records the diagnostic and applies the declared failure lifecycle.
Cancellation And Timeout
Commands receive an AbortSignal and may register cleanup callbacks:
execute({ input, signal, cleanup }) {
const request = checkoutApi.submit(input, { signal });
cleanup(() => request.releaseResources());
return request;
}
Cancel one command or every running command:
checkout.cancel('submit');
checkout.cancel();
Declare timeout duration and outcome with the command:
submit: {
from: 'ready',
pending: 'processing',
commandTimeout: 5000,
execute: submitOrder,
success: 'completed',
failure: 'failed',
cancelled: 'cancelled',
timeout: 'timed-out',
}
When cancelled or timeout is omitted, the command uses its failure
lifecycle.
Concurrency And Retry
Concurrency belongs to the declaration:
submit: {
from: ['ready', 'processing'],
pending: 'processing',
concurrency: 'reject',
retry: 2,
execute: submitOrder,
success: 'completed',
failure: 'failed',
}
rejectis the default and rejects another run of the same command.queueexecutes runs of the same command in request order.parallelallows overlapping execution when the declared source states also permit it.retryis the number of automatic retries after unexpected execution failure.
Callers cannot override these properties through run(). Operational behavior
therefore remains part of the workflow definition.
Diagnostics And History
workflow.diagnostics contains normalized, serializable evidence from failed
operations. workflow.history records command start, completion, and final
failure.
workflow.diagnostics;
workflow.history;
Use diagnosticLimit and historyLimit to bound retained entries:
app.workflow('checkout', {
initial: 'ready',
data: {},
diagnosticLimit: 100,
historyLimit: 500,
commands: checkoutCommands,
});
Snapshot And Restore
Snapshots contain the workflow id, state, data, diagnostics, and history:
const snapshot = checkout.snapshot();
localStorage.setItem('checkout', JSON.stringify(snapshot));
checkout.restore(JSON.parse(localStorage.getItem('checkout')));
Use migrateSnapshot when persisted data predates the current snapshot shape:
migrateSnapshot(snapshot) {
if (snapshot.version === 0) {
return {
version: 1,
id: 'checkout',
state: snapshot.stage,
data: snapshot.data,
diagnostics: [],
history: [],
};
}
return snapshot;
}
Supervisor
A workflow supervisor owns persistence and recovery for a named group:
app.workflowSupervisor('checkoutProcesses', {
workflows: {
checkout: checkoutDefinition,
fulfillment: fulfillmentDefinition,
},
persistence: 'indexeddb',
autoPersist: true,
autoRecover: true,
});
app.controller('AdminCtrl', ["checkoutProcesses", function (checkoutProcesses) {
this.checkout = checkoutProcesses.workflow('checkout');
}]);
Supervisors can also use a persistence adapter:
const persistence = {
async load(id) {
return database.load(id);
},
async save(id, snapshot) {
await database.save(id, snapshot);
},
};
autoPersist saves after every settled command. autoRecover restores the
latest snapshot and reruns the latest recoverable failed command for each
workflow.
Worker Runtime
The optional workflow worker runtime transports run, snapshot, and
restore. It does not define another orchestration protocol:
const host = createWorkflowWorkerHost({
workflows: { checkout },
});
const client = createWorkflowWorkerClient(workerConnection);
const result = await client.run('checkout', 'submit', order);
Runtime API
workflow.id;
workflow.state;
workflow.data;
workflow.diagnostics;
workflow.history;
workflow.can('submit');
workflow.run('submit', order);
workflow.cancel('submit');
workflow.snapshot();
workflow.restore(snapshot);
The runtime intentionally has no send(), manual lifecycle transition, or
per-run orchestration options.