Browser Boundaries
Browser boundaries keep direct browser access out of your craftService implementations while still making those dependencies explicit in the service graph.
Every boundary on this page is backed by a global crafted service marked with browserBoundary: true.
WARNING
Some APIs are not documented here yet.
Import
The main DSL exports are:
import {
BrowserCrypto,
BrowserDocument,
BrowserHistory,
BrowserLocation,
BrowserNavigator,
BrowserPerformance,
BrowserWindow,
Console,
Cookies,
LocalStorage,
SessionStorage,
} from '@craft-ng/core';When you need to derive methods for later reuse, each boundary also exposes the usual generated helpers:
import {
ConsoleServiceToYield,
injectConsoleService,
CONSOLE_SERVICE_META_DATA,
} from '@craft-ng/core';The same pattern exists for the other boundaries:
LocalStorageServiceToYieldSessionStorageServiceToYieldCookiesServiceToYieldBrowserLocationServiceToYieldBrowserHistoryServiceToYieldBrowserNavigatorServiceToYieldBrowserPerformanceServiceToYieldBrowserCryptoServiceToYieldBrowserDocumentServiceToYieldBrowserWindowServiceToYield
Motivation
Direct browser access inside a service hides dependencies inside business logic and makes tracking harder.
Browser boundaries solve that in two complementary ways:
- use
yield* X.method(...)when the browser interaction should happen directly inside the generator - use
XServiceToYield(...)when you want to derive bound browser helpers and reuse them inside returned callbacks
Mental Model
There are two valid ways to use a browser boundary.
Direct DSL
Use the DSL when the browser interaction belongs to the generator itself.
import { Console, craftService } from '@craft-ng/core';
const { injectBootLogger } = craftService(
{ name: 'BootLogger', scope: 'global' },
function* () {
yield* Console.log('boot');
yield* Console.info('config loaded');
return {
ready: true,
};
},
);Derived Service Helper
Use XServiceToYield(...) when the browser method needs to stay callable later from a returned method.
import { ConsoleServiceToYield, craftService } from '@craft-ng/core';
const { injectAuditTrail } = craftService(
{ name: 'AuditTrail', scope: 'global' },
function* () {
const consoleService = yield* ConsoleServiceToYield(
undefined,
({ log, error }) => ({
log,
error,
}),
);
return {
trackUserAction: (action: string) =>
consoleService.log('user action', action),
trackFailure: (error: unknown) =>
consoleService.error('unexpected failure', error),
};
},
);That second form is what preserves derivability while still tracking the browser dependency explicitly.
Core Examples
Console
yield * Console.log('my service run');
yield * Console.error('unexpected failure', error);Local Storage
yield * LocalStorage.setItem('token', token);
const persistedToken = yield * LocalStorage.getItem('token');
const entryCount = yield * LocalStorage.length();Session Storage
yield * SessionStorage.setItem('active-tab', 'settings');
const tab = yield * SessionStorage.getItem('active-tab');Cookies
yield *
Cookies.set('session', sessionId, {
path: '/',
sameSite: 'strict',
});
const session = yield * Cookies.get('session');
const hasSession = yield * Cookies.has('session');Location
const href = yield * BrowserLocation.href();
const pathname = yield * BrowserLocation.pathname();
yield * BrowserLocation.reload();History
yield * BrowserHistory.replaceState({ step: 2 }, '', '/checkout?step=2');
const state = yield * BrowserHistory.state();Document
yield * BrowserDocument.setTitle('Checkout');
const title = yield * BrowserDocument.title();Window
const width = yield * BrowserWindow.innerWidth();
yield * BrowserWindow.scrollTo(0, 0);
yield * BrowserWindow.alert('Cache cleared! The page will reload.');
const confirmed =
yield * BrowserWindow.confirm('Cache cleared! The page will reload.');
if (confirmed) {
yield * BrowserLocation.reload();
}Performance And Crypto
const now = yield * BrowserPerformance.now();
const uuid = yield * BrowserCrypto.randomUUID();API Reference
Every service below is:
scope: 'global'browserBoundary: true- exposed both as a DSL object and as generated service helpers
Console and ConsoleService
Methods:
debuginfologwarnerrortracegroupgroupCollapsedgroupEndtimetimeEnd
Generated helpers:
injectConsoleServiceConsoleServiceToYieldCONSOLE_SERVICE_META_DATA
LocalStorage and LocalStorageService
Methods:
getItemsetItemremoveItemclearkeylength
SessionStorage and SessionStorageService
Methods:
getItemsetItemremoveItemclearkeylength
Cookies and CookiesService
Methods:
getgetAllsetremovehas
BrowserLocation and BrowserLocationService
Methods:
hreforiginprotocolhosthostnameportpathnamesearchhashassignreplacereload
BrowserHistory and BrowserHistoryService
Methods:
lengthstatebackforwardgopushStatereplaceState
BrowserNavigator and BrowserNavigatorService
Methods:
userAgentlanguagelanguagesonLinecookieEnabledsendBeacon
BrowserPerformance and BrowserPerformanceService
Methods:
nowmarkmeasureclearMarksclearMeasures
BrowserCrypto and BrowserCryptoService
Methods:
randomUUIDgetRandomValuesdigest
BrowserDocument and BrowserDocumentService
Methods:
titlesetTitlevisibilityStatehasFocus
BrowserWindow and BrowserWindowService
Methods:
innerWidthinnerHeightscrollXscrollYscrollToalertconfirm
Related Adapter: CraftHttpClient
CraftHttpClient is implemented, but it is not a browser boundary.
Unlike Console, LocalStorage, or BrowserLocation, Angular's HttpClient is already a DI-managed Angular dependency. It is better modeled as a typed craft adapter than as a browser-host global.
Its contract is intentionally different:
- it is not treated as
browserBoundary: true - it requires
success: response<T>()inside a declarative builder - it can declare ordered
exceptions: [function* (...) { ... }]rules - it returns a promise of
Success | craftException({ code: 'HttpError' })
Usage looks like this:
const getUsers =
yield *
CraftHttpClient.get(({ response }) => ({
url: '/api/users',
params: { page: 1 },
success: response<User[]>(),
}));
const createUser =
yield *
CraftHttpClient.post(({ response }) => ({
url: '/api/users',
payload,
success: response<User>(),
}));
const login =
yield *
CraftHttpClient.post(({ response }) => ({
url: '/api/login',
payload,
success: response<{ token: string }>(),
exceptions: [
function* ({ status, code, content }) {
if (!(yield* status(400))) return;
if (!(yield* code('PASSWORD_REQUIRED'))) return;
if (!(yield* content('Password is required'))) return;
return craftException({ code: 'PASSWORD_REQUIRED' });
},
],
}));
const users = await getUsers();
const createdUser = await createUser();
const loginResult = await login();Design Constraints
The browser boundaries stay intentionally narrow.
- Reads are exposed as methods so the public API stays uniform with
yield*. - Raw
window,document, and DOM nodes are not exposed as public outputs. BrowserDocumentandBrowserWindowremain minimal rather than becoming generic escape hatches.
This keeps the API focused on explicit browser interactions instead of reintroducing broad direct access to host globals.
Relationship With craftService And toCraftService
Browser boundaries participate in the same dependency tracking model as any other crafted service.
craftServiceis what you use to consume them and compose higher-level services.toCraftServiceremains the right tool for adapting Angular or host dependencies that are not part of this built-in browser boundary set.