Data sources and resource loading
Core reads data, resolves resource paths and manages loading and cleanup. It retains your data format; Vega, Cassiopeia and Home Spot own their respective visuals. Start with a real JSON file and image here, then use the Vega guide for playback. The hosted https://haneoka.org/embed/core.js runs directly in a browser without npm installation.
1. Read an existing JSON file
Section titled “1. Read an existing JSON file”Open the complete promenade.json. It contains a title, background path and one text line; garden.svg is its original image. Select HTTP JSON in the application below and load to see the data. Resolve resource URL obtains the image address; Read resource bytes reads its actual content. This lines document is reader data rather than a playable Vega document; step 3 converts this specific format.
Edit the JSON parameters, then load. Modules and requests start only after a click; results include the actual response and transport log.
Preparing example…
View the example source import { createEmbedLoader, httpDataSource } from "https://haneoka.org/embed/core.js";const loader = createEmbedLoader({ source: httpDataSource({ url: "https://docs.haneoka.org/examples/promenade.json" }), locale: "fr",});const data = await loader.load();const imageUrl = await loader.resourceUrl(data.background);// After the consumer finishes with this address: await loader.dispose().HTTP resources resolve against the configured JSON directory, which redirects do not change. An explicit assetsBase overrides it. resourceUrl obtains an address; resourceBytes reads the content. Panel fields select factories and resources. In particular, resourceKey/resourceField are this example’s resource-selection fields, while the complete SDK options are listed below.
2. Select a real local JSON and image
Section titled “2. Select a real local JSON and image”Save promenade.json and garden.svg. Open the complete file reader, select the JSON and image, then click Load files. You should see Une promenade, the original garden image and the actual image byte count. Close reader removes the image before disposing its loader and Blob URL; select new files and load again.
Save all the code below as UTF-8 index.html to keep this project. The quickstart explains local HTTP opening. Its folder contains index.html, promenade.json and garden.svg. The JSON’s background is a resource key; the map binds that literal key to your selected File, including legal Unicode paths.
<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Read local JSON and its image</title><style>body{max-width:60rem;margin:auto;padding:1rem;font:1rem/1.5 sans-serif;color:CanvasText;background:Canvas}button,input{font:inherit}button,label{min-height:48px}label{display:block;margin:.5rem 0}img{width:320px;aspect-ratio:16/9;object-fit:contain}[hidden]{display:none!important}pre{white-space:pre-wrap}</style><p><a href="https://haneoka.org/">Haneoka</a></p><h1>Read local JSON and its image</h1><p>Select promenade.json and garden.svg, then load them together.</p><label>JSON file <input id="json-file" type="file" accept=".json,application/json"></label><label>Image file <input id="image-file" type="file" accept="image/*,.svg"></label><button id="load">Load files</button><button id="cancel" disabled>Cancel</button><button id="close">Close reader</button><p id="status" role="status" aria-live="polite">Choose the two files.</p><h2 id="title"></h2><img id="preview" hidden alt="Image from the selected file"><pre id="output"></pre><script type="module">const by=id=>document.getElementById(id); let loader,controller,generation=0;async function close(){generation++;controller?.abort();by("preview").hidden=true;by("preview").removeAttribute("src");by("title").textContent="";by("output").textContent="";const old=loader;loader=undefined;await old?.dispose();by("cancel").disabled=true;}by("load").onclick=async()=>{ by("load").disabled=true;let current; try{ await close();current=++generation;controller=new AbortController();const signal=controller.signal;by("cancel").disabled=false;by("status").textContent="Reading…"; const jsonFile=by("json-file").files[0],imageFile=by("image-file").files[0]; if(!jsonFile||!imageFile)throw new Error("Choose the JSON and image first."); const {createEmbedLoader,fileDataSource,fileResourceResolver}=await import("https://haneoka.org/embed/core.js"); signal.throwIfAborted();if(current!==generation)return; const files=new Map(); loader=createEmbedLoader({source:fileDataSource(jsonFile),locale:"fr",maxBytes:1048576,resolveResource:fileResourceResolver(files)}); const data=await loader.load({signal}); if(typeof data.background!=="string"||!data.background)throw new Error("The JSON needs a background resource key."); files.set(data.background,imageFile); const bytes=await loader.resourceBytes(data.background,{signal}); const url=await loader.resourceUrl(data.background,{signal}); if(current!==generation)return; by("title").textContent=String(data.title??"");by("output").textContent=JSON.stringify({data,imageBytes:bytes.length},null,2); by("preview").src=url;by("preview").hidden=false;await by("preview").decode(); if(current===generation)by("status").textContent="JSON and selected image loaded."; }catch(error){if(current===generation){by("status").textContent=`Read failed: ${error.message}`;const failed=loader;loader=undefined;by("preview").removeAttribute("src");by("preview").hidden=true;await failed?.dispose().catch(()=>{});}} finally{if(current===generation){by("load").disabled=false;by("cancel").disabled=true;}}};by("cancel").onclick=()=>{controller?.abort();};by("close").onclick=async()=>{await close();by("load").disabled=false;by("status").textContent="Reader closed. You can load again.";};window.addEventListener("pagehide",()=>{void close();},{once:true});</script></html>Select the JSON and its required resources separately. This example maps one image; larger projects map every declared resource key, including model sidecars, to a selected File.
3. Convert this reader data into a story
Section titled “3. Convert this reader data into a story”promenade.json is this page’s data format. Vega requires an AdvStory commands array. For this particular lines structure, create this object after reading data:
const storyDocument = { localization: { locales: [data.language], defaultLocale: data.language }, commands: [ { command: 25, background: { url: data.background } }, ...data.lines.map(line => ({ command: 2, targetName: line.speaker, text: line.text })), ],};Pass storyDocument as mountStory’s document and use that same resource directory as assetsBase. Vega supplies the complete browser page and load steps. If your selected JSON is already a complete AdvStory, read it directly instead of applying this lines conversion. A decoder validates or converts actual data; a TypeScript type only describes your expectation.
4. Read Haneoka current data
Section titled “4. Read Haneoka current data”Choose Haneoka in the example and load its public song before resolving jacketUrl. Both reader and catalog adapter are available from the hosted entry:
import { createEmbedLoader, haneokaDataSource } from "https://haneoka.org/embed/core.js";const loader = createEmbedLoader({ source: haneokaDataSource({ resource: "songs", id: "100070" }), server: "intl", locale: "en",});const song = await loader.load();const imageUrl = await loader.resourceUrl(song.jacketUrl);// End the consumer before awaiting loader.dispose().Public reads require no account or release. Server chooses the registry and locale selects the language context; the catalog retains its supplied localized fields. The source’s resource base defaults to the API origin, and loader.assetsBase can override it. Repeated load reads current data again. Use memoryDataSource for an existing object and fileDataSource for a selected File; their complete signatures and transport constraints follow.
All createEmbedLoader options
Section titled “All createEmbedLoader options”| Field | Type/required/default | Effect, combinations and errors |
|---|---|---|
source | required DataSource<T> | Must supply load(context); loader calls it for each load. Optional source assetsBase supplies the resource directory, optional dispose runs once. Choose the source appropriate to the consuming host’s format. |
server | optional string; undefined | Passed unchanged to custom sources/resolvers. Haneoka adapter defaults it to intl and validates the registry slug through the public client. Language never chooses a server. |
locale | optional string; en | Passed unchanged; author locales such as fr/pt-BR are valid. Haneoka sends it as Accept-Language for supported-language negotiation. |
assetsBase | optional string; source.assetsBase, then none | Overrides source base. Absolute HTTP(S) directory, no query/fragment; trailing slash added. Invalid base throws TypeError at construction. Relative keys without a base fail URL construction. |
resolveResource | optional (key:string,context:DataContext)=>string|URL|Blob|Promise<…>; key unchanged | Completely maps a resource key. Result URL uses assetsBase when relative. Resolver rejection rejects that resource operation; missing local Map entry does not fall through to HTTP. |
fetcher | optional (Request)=>Promise<Response>; fetch | Receives cors Request, caller headers, configured credentials and signal. Both document and resource reads use it. A downstream media element has its own transport. Throwing rejects the operation. |
credentials | optional omit/same-origin/include; omit | Applies to loader transport. Credentialed cross-origin services must allow the embedding origin and credentials. Invalid values are rejected by native Request. |
headers | optional HeadersInit; none | Shared transport headers; a source’s per-request headers override matching names. Haneoka adapter sets Accept-Language from locale. Headers validation follows Fetch. |
maxBytes | optional positive safe integer; 67108864 | Per decoded JSON/resource-byte response budget, not a total scene/cache/URL-resolution budget. Nonpositive/unsafe values throw RangeError at construction; oversized reads throw RangeError (Haneoka wraps response_too_large). |
onListenerError | optional (error:unknown)=>void; ignored | Receives observer exceptions. Its own exception is isolated, and never changes the operation result. |
A loader has no constructor signal, timeout, decoder or automatic retry/cache option. Supply a per-operation signal, a source decoder, or a transport/source policy. Resolving a URL alone does not download bytes or apply maxBytes to the eventual media element.
All source factories
Section titled “All source factories”| Factory | Required arguments | Optional arguments/defaults | Resource origin and validation |
|---|---|---|---|
memoryDataSource(data) | any T, including an authored document | none | Returns the exact original value/reference; no default asset directory or cloning/validation. |
httpDataSource({url,decode?}) | absolute HTTP(S) string or URL | decode(unknown)→T; default parsed JSON | Factory validates protocol/URL. Base is configured URL’s directory, not redirect destination. Loader maxBytes/signal apply; JSON parse/decode failures reject load. |
fileDataSource(file,decode?) | browser File or Blob, including a host-created Blob | same decoder; default parsed JSON | No network base. Reads actual Blob bytes through loader budget/progress; invalid JSON/decoder rejects load. |
fileResourceResolver(files) | ReadonlyMap<string,Blob> | none | Exact Unicode key lookup, no normalization. Missing key throws Error. URL calls create owned Blob URLs; byte calls read Blob directly. |
haneokaDataSource({resource,id?,view?,apiBase?,decode?}) | resource string | details below | Current public catalog adapter; no account or release parameter. |
Haneoka factory fields: resource is a manifest name with public-client resource syntax; id?:string selects an entity (omitted→index); view?:string selects a manifest projection; apiBase?:string defaults https://haneoka.org/api/v1/ and must be an absolute HTTP(S) directory without query/fragment; decode? validates/converts the index/entity (default JSON value). Syntax/absent data errors follow client rules during load. Source asset base is the API origin root; a loader assetsBase explicitly overrides it. Server and locale belong to the loader context rather than this factory.
Complete custom source and resolver context
Section titled “Complete custom source and resolver context”A DataSource<T> declares load(context): T|Promise<T>, optional assetsBase?:string, and optional dispose():void|Promise<void>. The loader invokes cleanup once during disposal and waits for it; cleanup rejection rejects the shared disposal promise. Use a fresh owned source when its cleanup releases resources used by another loader.
DataContext contains these six public fields:
| Field | Type | Meaning |
|---|---|---|
| server | optional string | Configured source registry, unchanged. |
| locale | string | Configured/default author language. |
| signal | AbortSignal | That operation’s cancellation signal; use it in your work. |
| fetcher | (Request)→Promise<Response> | Loader’s configured transport. |
| maxBytes | number | Effective decoded-byte budget; custom parsing should honor it. |
| progress | ({loaded:number,total?:number})→void | Reports actual operation bytes; omit total when unknown. |
progress’s argument has its own loaded/optional total fields. The resolver returns a string, URL or Blob synchronously/asynchronously. Absolute HTTP/HTTPS/Blob/data URLs keep their origin; relative strings use the effective assetsBase and / starts at its origin root. Other protocols throw TypeError. The loader never invents a browser/SSR page base.
const source = { assetsBase: "https://author.example/project/", async load(context) { context.signal.throwIfAborted(); const response = await context.fetcher(new Request("https://author.example/project/story.json", { signal: context.signal })); if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json(); // use your format-specific bounded decoder here }, dispose() { /* release resources owned by this source */ },};const loader = createEmbedLoader({ source, locale: "pt-BR", resolveResource: key => key });Every handle method and property
Section titled “Every handle method and property”| Member | Arguments/defaults and result | Lifetime behavior |
|---|---|---|
locale | readonly string | Effective locale. |
server | readonly string or undefined | Explicit server, before an adapter applies its fallback. |
disposed | readonly boolean | Becomes true immediately when dispose starts. |
load(options={}) | optional {signal?:AbortSignal}; Promise<T> | New independent source operation, key $data; repeated loads call source again. |
resourceUrl(key,options={}) | required string + optional signal; Promise<string> | Resolves key, no byte fetch. One cached URL per owned Blob until dispose. |
resourceBytes(key,options={}) | required string + optional signal; Promise<Uint8Array> | Resolves key and reads bytes with status/budget/progress. |
subscribe(listener) | required (event:EmbedEvent)=>void; returns unsubscribe function | Receives future events; does not replay state. Throws if already disposed. |
cancel() | no arguments; void | Aborts all current operations; leaves loader reusable. |
dispose() | no arguments; shared Promise<void> | Immediately disallows work, aborts active calls, revokes owned URLs, emits disposed, clears observers and invokes source cleanup once. Idempotent; disposed loader cannot be reused. |
Per-call signal cancellation rejects promptly, including when a custom source ignores its signal. The loader suppresses late ready/progress. Source work should still observe context.signal to stop its underlying activity. cancel() preserves later calls; dispose() ends the loader. Release consumers before revoking URLs; URLs authored by your own code remain your responsibility.
Complete events and failures
Section titled “Complete events and failures”| Event type | Fields | When |
|---|---|---|
| start / ready | operation:number, key:string | Begin / successfully finish an operation. |
| progress | operation,key,loaded:number,total?:number | Bytes actually read. Memory data/URL-only resolution have no automatic byte progress. Compression/unknown Content-Length omit total. |
| error / cancelled | operation,key,error:unknown | Failure / cancellation. |
| disposed | type only | Loader disposal; cancellation failures are suppressed after disposal. |
IDs increase independently per loader; key is $data or the literal resource key. Listener errors do not reject operations. Concurrent operations are independent; there is no data-level deduplication or retry. The small demo retains at most32 displayed records while the SDK’s event stream remains unchanged.
EmbedHttpError(status,url) extends Error with name/message/status/url for a non-success HTTP/resource read. Invalid URLs/bases/resolved protocols throw TypeError, exceeded byte budgets throw RangeError, invalid JSON throws SyntaxError, and authored decoder/source/resolver errors remain theirs. Haneoka API failures retain ApiClientError; its response budget reports code response_too_large, HTTP status and nonretryable default. Abort rejects with the signal reason, usually AbortError/TimeoutError. Calls after dispose reject Error; source cleanup failure rejects dispose.
Optional branding helper
Section titled “Optional branding helper”createHaneokaBranding(document,options={}) is exported by the hosted core facade and the package /branding entry. Required document is the host DOM Document; corner?:"top-left"|"top-right"|"bottom-left"|"bottom-right" defaults top-left. It returns an HTMLAnchorElement, makes no image request, and does not append itself. Append it as a translucent overlay to a positioned full-size host, preserving its48px touch target, and remove it during cleanup. The href is fixed https://haneoka.org/, target _blank with noopener; original inline SVG/48px touch target/focus are supplied. Visual embed hosts manage this helper automatically. Importing data-only core creates no DOM.