Documentation
Backend adapters, asset serving, cache behavior, and example route contracts for solverforge-ui.
Integration & Assets
This page summarizes how solverforge-ui connects frontend code to backend APIs
and how static assets are delivered.
Backend Adapters
Create adapters with SF.createBackend(...) and pass the result into
SF.createSolver(...).
Axum (default)
var backend = SF.createBackend({ type: "axum", baseUrl: "" });
Use this when your backend exposes the stock solverforge-ui lifecycle
contract. New integrations should model retained jobs and snapshots rather than
build around schedule-specific naming.
Tauri Adapter
var backend = SF.createBackend({
type: "tauri",
invoke: window.__TAURI__.core.invoke,
listen: window.__TAURI__.event.listen,
eventName: "solver-update",
});
Use this when solver traffic is bridged through Tauri IPC.
Generic Fetch Adapter
var backend = SF.createBackend({
type: "fetch",
baseUrl: "/api/v1",
headers: { "X-CSRF-Token": csrfToken },
});
Use this when your app needs extra headers or a non-default base path while
still implementing the retained-job backend methods expected by
SF.createSolver(...).
Lifecycle Contract Expectations
The shared lifecycle model is job-oriented:
- create job
- get job
- get job status
- stream job events
- get snapshot
- analyze snapshot
- pause job
- resume job
- cancel job
- delete job
- get demo data
Older articles may mention schedule-named routes. Current generated apps expose
/jobs/...; new integrations should use the job vocabulary directly.
The create operation may resolve to either:
- a non-empty plain job id string
- a finite numeric job id, including
0 - an object containing a scalar
id,jobId, orjob_id
SF.createSolver(...) normalizes accepted create-job identifiers to strings
before stream attachment. Empty strings, non-finite numbers, missing object
fields, arrays, and nested object identifiers reject startup instead of being
silently stringified.
Current backend expectations are:
getSnapshot()andanalyzeSnapshot()accept an optionalsnapshotRevisionpauseJob()requests a pause, butsolver.pause()resolves only after the authoritativepausedevent and snapshot sync completeresumeJob()settles on the authoritativeresumedeventcancelJob()is the backend operation behind user-facing Stop and settles after the terminal lifecycle event has been synchronizeddeleteJob()is required for every backend passed toSF.createSolver(...)and is destructive cleanup for terminal retained jobs only- streamed events should use canonical camelCase fields:
eventType,jobId,eventSequence,lifecycleState,snapshotRevision,currentScore,bestScore,telemetry, andsolutionwhere required - supported
eventTypevalues areprogress,best_solution,pause_requested,paused,resumed,completed,cancelled, andfailed - raw score-only progress payloads and implicit completion heuristics are not part of the supported stream contract
Solver Lifecycle
SF.createSolver(...) builds the client-side retained-job state machine on top
of the backend adapter.
var solver = SF.createSolver({
backend: backend,
statusBar: statusBar,
onProgress: function (meta) {
renderTelemetry(meta);
},
onSolution: function (snapshot, meta) {
render(snapshot.solution, meta);
},
onPaused: function (snapshot, meta) {
render(snapshot.solution, meta);
},
onComplete: function (snapshot, meta) {
render(snapshot.solution, meta);
},
});
Treat the shipped solver helper as a lifecycle controller for one retained job: it starts work, observes authoritative lifecycle events, renders snapshots, and coordinates pause, resume, cancel, analysis, and terminal cleanup through the backend adapter.
start() never replaces an existing retained job. Even after completion,
cancel, or failure, call delete() and wait for successful backend cleanup
before starting the next solve.
The current solver surface returns:
start(data)pause()resume()cancel()delete()getSnapshot(snapshotRevision?)analyzeSnapshot(snapshotRevision?)isRunning()getJobId()getLifecycleState()getSnapshotRevision()
Supported callbacks are onProgress, onSolution, onPauseRequested,
onPaused, onResumed, onCancelled, onComplete, onFailure, onAnalysis,
and onError.
Startup Stream Contract
Startup streams may begin with either a scored progress event or a scored
best_solution event. Consumers must not require progress to arrive first.
Runtime rules:
progressis metadata-only and must not carry the solution payloadbest_solutionmust include bothsolutionandsnapshotRevisionpause_requestedmeans the runtime accepted the request, not that the exact checkpoint is already availablepaused,completed,cancelled, andfailedare authoritative lifecycle events;SF.createSolver()synchronizes the retained snapshot before firing the corresponding callbacks- the status bar uses
currentScoreas the live score during solving - missing or malformed typed lifecycle fields are ignored instead of being silently normalized
- HTTP
EventSource.onerroris transport state, not runtime lifecycle state; transient reconnecting errors do not change the lifecycle - a closed SSE stream surfaces through
onErrorwhile preserving the last authoritative lifecycle, retained job id, score metadata, and snapshot revision delete()waits for required terminal snapshot synchronization before callingdeleteJob(); if synchronization or backend deletion fails, the retained job id and terminal lifecycle state remain intact- Stop remains visible during
CANCELLINGso the UI can reattach a detached stream listen-only, but it must not send a duplicatecancelJob()
Asset Serving Under /sf/*
solverforge_ui::routes() serves GET /sf/{*path} from the crate’s embedded
asset directory.
Common assets include:
/sf/sf.css/sf/sf.js/sf/sf.0.6.5.css/sf/sf.0.6.5.js/sf/vendor/fontawesome/css/fontawesome.min.css/sf/vendor/fontawesome/css/solid.min.css
Cache Behavior and Versioned Bundles
The crate emits both stable and versioned bundle filenames:
- stable:
/sf/sf.css,/sf/sf.js - versioned:
/sf/sf.<crate-version>.css,/sf/sf.<crate-version>.js
src/lib.rs serves them with different cache policies:
- stable bundles use
Cache-Control: public, max-age=3600 - versioned bundles use
Cache-Control: public, max-age=31536000, immutable fonts/,vendor/, andimg/assets are also served as immutable
A practical strategy is to use stable URLs during development and versioned bundles in production or CDN environments.
Optional Modules
When the optional map module is shipped in static/sf/modules/, include it
alongside Leaflet:
<link rel="stylesheet" href="/sf/vendor/leaflet/leaflet.css" />
<script src="/sf/vendor/leaflet/leaflet.js"></script>
<link rel="stylesheet" href="/sf/modules/sf-map.css" />
<script src="/sf/modules/sf-map.js"></script>
For route geometry, travel-time, and map-data pipeline details, see the existing solverforge-maps docs.
Map Helper Surface
The optional map module exposes SF.map after Leaflet and sf-map.js are
loaded:
var map = SF.map.create({
container: "map",
center: [45.07, 7.69],
zoom: 13,
});
map.addVehicleMarker({ lat: 45.07, lng: 7.69, color: "#10b981" });
map.addVisitMarker({
lat: 45.08,
lng: 7.7,
color: "#3b82f6",
icon: "fa-utensils",
});
map.addStopNumber({ lat: 45.08, lng: 7.7, number: 1, color: "#3b82f6" });
map.drawRoute({
points: [
[45.07, 7.69],
[45.08, 7.7],
],
color: "#10b981",
});
map.drawEncodedRoute({ encoded: "encodedPolylineString", color: "#3b82f6" });
map.fitBounds();
map.highlight("#10b981");
map.clearHighlight();
map.clearRoutes();
map.clearStops();
map.clearMarkers();
map.clearAll();
fitBounds() fits vehicle and visit markers. Since 0.6.2, marker bounds are
backed by a Leaflet feature group, so marker-only maps can call fitBounds()
without needing route polylines first.
Non-Rust Integration Path
static/sf/ is self-contained. If you are not serving assets from Rust, copy,
submodule, or symlink it into your static-files directory so /sf/* resolves
the same way in production.