Asset Manager

TurboWarp custom extension · v0.13.0

Use every project asset through one name.

Asset Manager registers web images and audio, project costumes, backdrops, sounds, and live text. Your scripts can then display, play, animate, cache, and replace them through one consistent set of blocks.

URL or project resource
Named asset
Named asset
Sprite, stage, or sound

Kamishibai DSL 4.0 bundle

The bundled blocks use this same reference.

In the DSL 4.0 runtime palette, find the Asset Manager member heading and click its documentation button. It opens this guide. The bundled blocks keep Asset Manager's behavior and names; the bundle only adds a member-specific namespace and icon.

Quick start

Load, register, use.

Asset Manager must run without the extension sandbox because it works with TurboWarp's renderer, audio system, and project resources.

  1. Load the extension

    In TurboWarp, choose Add Extension → Custom Extension and paste:

    https://cdn.jsdelivr.net/npm/@kubohiroya/turbowarp-asset-manager@0.13.0/dist/asset-manager.js

    Enable Run extension without sandbox.

  2. Register a resource

    Give the resource a short name that the rest of the project will use.

    register resource [https://example.com/hero.png]
      as asset [Hero]
  3. Use the named asset

    The same name now works in display, sound, and animation blocks when its type fits.

    show asset [Hero] on this sprite

How it works

One registry connects every source to the right output.

Registration records the asset's type and location under the name you choose. Later blocks resolve that name and select the correct renderer, audio, or text path automatically.

From a resource identifier to a visible or audible result

Web URLs, project resources, and runtime text enter the register resource block. The asset registry remembers their chosen names and types. A display, play, or animate block then routes each asset to a sprite, stage, sound output, or Animated Text.

Which resource identifier should I use?

SourceResource identifierWhat registration keeps
Web image or audio https://example.com/asset.png Downloaded bytes in memory and IndexedDB
Sprite costume costume:Sprite1:costume1 A reference to the existing project costume
Stage backdrop backdrop:backdrop1 A reference to the existing backdrop
Sprite or stage sound sound:Sprite1:sound1 or sound:@stage:name A reference to the existing project sound
Live text text:Narration The runtime-variable name; the current value is read when shown
Previously cached web asset Leave RESOURCE_ID empty Bytes loaded from IndexedDB under the asset name

How web assets and the cache work

Supplying a URL always fetches fresh data and refreshes the named cache entry. Supplying an empty resource identifier skips the network and reloads that same name from IndexedDB. The per-name generation is checked before a fetched response may update IndexedDB or the in-memory registry, so an older request that finishes late cannot overwrite the latest one.

Two ways to fill the in-memory registry

With a URL, Asset Manager fetches the resource, normalizes its media type, verifies that the request is still the latest generation, writes the named entry to IndexedDB, and registers it in memory. With an empty resource identifier, Asset Manager reads the named entry from IndexedDB and registers it in memory without a network request.

Exact project asset locators for composition hosts

Version 0.8.0 lets composition hosts register a costume, backdrop, or sound with a structured locator. Scratch names are matched exactly, including leading and trailing spaces, ., /, :, and control characters. Structured locators also preserve the logical registration name literally by default.

await assets.registerProjectAsset({
  name: 'Costume.1 / presentation',
  locator: {
    kind: 'costume',
    target: 'Actor / presenter',
    name: 'Costume.1 / source'
  }
});

A sound locator without target selects the Stage. The existing string resourceId form remains compatible and keeps its historical trimming and colon grammar. Embedded assets can request literal logical names with nameMode: 'literal'.

Verified remote cache for composition hosts

Version 0.6.0 adds a block-free, cache-first path for app shells and composite extensions. It is separate from the legacy name-keyed cache above. The host supplies the network loader and an expected SHA-256, byte size, and Content-Type. Asset Manager returns a valid IndexedDB hit without a network request; otherwise it downloads, verifies, and only then stores the bytes. Source URLs and credentials are not persisted.

import {
  createAssetManagerComposition,
  createVerifiedRemoteCacheDatabaseName
} from '@kubohiroya/turbowarp-asset-manager/composition';

const databaseName = createVerifiedRemoteCacheDatabaseName({
  id: story.cacheId,
  label: story.fileName
});
const assets = createAssetManagerComposition(undefined, {
  verifiedRemoteCache: {
    cacheIdentity: {id: story.cacheId, label: story.fileName, databaseName}
  }
});
const result = await assets.resolveVerifiedRemoteBinary(model, {
  load: async ({url}, {signal}) => {
    const response = await fetch(url, {signal});
    return {
      bytes: await response.arrayBuffer(),
      contentType: response.headers.get('content-type'),
      transferOwnership: true
    };
  }
});
Persistent cache lifetime and materialized memory lifetime are independent

The story manifest supplies a stable story ID and readable filename. Asset Manager uses them to identify a per-story IndexedDB database, verifies downloaded bytes, and returns them to the host. The host materializes an image, sound, or model and releases that in-memory resource according to scene or story retention without deleting the persistent verified bytes.

LayerLifetime and cleanupHost responsibility
Returned binary Owned by the caller after resolution Drop references after registration; transferOwnership: true avoids an extra full-size JavaScript copy.
Materialized memory retention: scene or retention: story Call releaseAsset or releaseAll when the policy expires.
Verified IndexedDB bytes TTL, LRU, origin budget, or explicit clear Offer cache visibility and clear controls; memory release does not delete this cache.
Story catalog and lease One readable database record per story; active runtimes are protected Renew while running and release the lease when the story or session stops.

Story databases use names such as tw-kamishibai-assets-v1--opening-yaml--story-0001. A small shared catalog stores only understandable identity, size, timestamps, and runtime leases—not asset binaries—and lets an app shell list, prune, or delete caches. Cleanup skips every database with an active lease. The default high-water mark is the smaller of 256 MiB and 20% of the reported origin quota; inactive story databases and least-recently-used records are removed first. Records unused for 30 days are eligible for removal. IndexedDB failures and pinned-origin budget pressure are reported as machine-readable warnings while verified bytes remain usable in memory.

Use listVerifiedRemoteStoryCaches, pruneVerifiedRemoteStoryCaches, and deleteVerifiedRemoteStoryCache for app-level storage management. Use renewVerifiedRemoteStoryCacheLease and releaseVerifiedRemoteStoryCacheLease for runtime ownership. The DSL values retention: scene and retention: story are host lifecycle policy; Asset Manager does not parse YAML or reinterpret them as IndexedDB TTL.

Transactional multi-file bundles

Version 0.7.0 adds a block-free binary bundle store for self-contained assets such as a Teachable Machine Pose model. Import the composition subpath, choose a story-specific database, and bind every operation to the story/source namespace, asset name, and manifest bundle integrity.

const assets = createAssetManagerComposition(undefined, {
  binaryBundleStore: {
    databaseName: `${story.cacheDatabaseName}--bundles-v1`
  }
});

await assets.putBinaryBundle({
  namespace: `${story.id}/${story.sourceIntegrity}`,
  name: 'RescuePose',
  integrity: pose.bundleIntegrity,
  files: [
    {path: 'model.json', size: model.size, integrity: model.integrity, bytes: model.bytes},
    {path: 'metadata.json', size: metadata.size, integrity: metadata.integrity, bytes: metadata.bytes},
    {path: 'weights.bin', size: weights.size, integrity: weights.integrity, bytes: weights.bytes}
  ]
});

const stored = await assets.getBinaryBundle({
  namespace: `${story.id}/${story.sourceIntegrity}`,
  name: 'RescuePose',
  integrity: pose.bundleIntegrity
});
A complete bundle becomes visible only after one transaction commits

The host supplies a story-scoped key and all bundle files. Asset Manager verifies every declared size and SHA-256, commits the bundle and metadata in one IndexedDB transaction, and returns the complete verified bundle on a later read. Missing, partial, or corrupt records fail closed.

putBinaryBundle resolves only after IDBTransaction.oncomplete and defensively owns the supplied bytes. getBinaryBundle rechecks file paths, sizes, and canonical SHA-256 values before returning data. deleteBinaryBundle removes data and metadata atomically; releaseBinaryStore aborts that store instance without deleting persistent records. TTL, LRU, bundle-count, and byte limits are enforced independently from the verified remote cache and the Standalone asset database. Failures expose stable ASSET_BINARY_BUNDLE_* codes without payload bytes.

Session-only backing for embedded packages

Version 0.10.0 adds a separate composition contract for large binaries that already ship inside the current application package. Unlike the persistent bundle store above, session backing never reuses records at the next startup. It uses one shared versioned database, includes a fresh session ID in every compound key, and removes only that session on normal disposal. The verified remote cache remains unchanged.

const backing = await assets.createSessionBinaryBacking({
  policy: 'prefer',
  sessionId: crypto.randomUUID(),
  assets: embeddedManifest.assets,
  source: embeddedPackageSource,
  onFatalError(error) {
    stopRuntimeAndShowDiagnostic(error);
  }
});

const pose = await backing.get(poseAssetKey);
await backing.dispose();
The startup mode is selected once and never changes during the session

Disabled mode reads the original source without opening IndexedDB. Prefer and required modes write one asset at a time and verify each committed record. Prefer can select direct mode only after a pre-establishment storage availability failure and clean partial records. After session mode is established, every read failure is fatal and never switches source.

disabled never opens IndexedDB. required fails startup when backing cannot be established. prefer reports a warning and keeps the same source for direct reads only when IndexedDB is unavailable, blocked, out of quota, or aborted before establishment; descriptor, size, and SHA-256 failures always fail closed. Session mode releases the source only after every asset commit and read-back succeeds. A later missing, corrupt, integrity-failing, aborted, or closed-connection read is fatal and never triggers a source reread, implicit rewrite, or mode change. Short heartbeat leases and bounded startup cleanup reclaim crashed sessions without deleting an unexpired sibling tab. Renderer, audio-decoder, and model-loader failures remain separate materialization diagnostics.

How safe same-name replacement works

Replacement rollout is controlled by two startup-fixed flags. Both default to false. Define them before loading the extension bundle when a project is ready to opt in:

globalThis.__TW_ASSET_MANAGER_FEATURE_FLAGS__ = {
  ENABLE_LIVE_ASSET_REPLACEMENT: true,
  ENABLE_STRICT_ASSET_KIND_REPLACEMENT: true
};
A successful live replacement commits only after the new content is ready

Asset Manager first prepares the new content, then updates the registry, reapplies it to only the targets still bound to that asset name and public kind, and finally releases the old owned resource. If preparation or display reapply fails, the old registration, display, and cache are restored.

Images and text refresh immediately. Text rereads its latest body and full style, and starts a configured animation again from the beginning. Audio already playing continues; the next playback uses the replacement. Same-name commits run serially, so an active commit finishes atomically and the last-started successful registration remains afterward. A newer invalid attempt does not cancel an earlier valid one. For images, Asset Manager also verifies the renderer skin it applied before refreshing a target. A later costume or renderer change releases that stale display binding instead of being overwritten. With strict-kind replacement enabled, the public kinds external, costume, backdrop, sound, and text cannot replace one another. External image/audio changes are rejected too. Delete the old registration first when a kind change is intentional.

Diagnostic error codes

Upgrading from 0.3.0: update scripts that compare asset registration error type against the previous lowercase type tokens. Version 0.4.0 returns the uppercase diagnostic codes below.

User-facing errors begin with [Asset Manager][CODE] and retain the operation, relevant asset/resource/actor names, a correction hint, nearby candidates, and the original cause. The registration error reporters expose the latest code and its most relevant label. Synchronous failures are thrown without duplicate logging; background playback and animation failures are sent to console.error once.

CodeWhat to check
INVALID_ASSET_NAMEUse a non-empty valid registration name.
ASSET_NOT_REGISTEREDRegister the requested name; inspect suggested nearby names.
ASSET_TYPE_MISMATCHUse the image, audio, or text kind required by the operation.
ASSET_TYPE_CHANGEDelete first before intentionally changing a public or external media kind.
SPRITE_NOT_FOUNDCheck the actor or target name and that its drawable still exists.
SPRITE_NAME_AMBIGUOUSGive actor targets unique names.
SOURCE_ASSET_NOT_FOUNDCheck the costume, backdrop, or sound name.
RESOURCE_ID_INVALIDUse HTTP(S) or a supported project-resource identifier.
DEPENDENCY_MISSINGLoad the required TurboWarp extension or runtime service.
STYLE_PROPERTY_INVALIDUse a supported text style property.
STYLE_VALUE_INVALIDCorrect the stored text style value.
PLAYBACK_FAILEDCheck audio data and browser playback permission.
ANIMATION_FAILEDCheck the actor, assets, and Animated Text dependency.
REPLACEMENT_FAILEDCheck loading or display preparation; the old state is retained.

How an actor animation is scheduled

ASSETS and DURATIONS describe a timeline. Images change the actor's skin; audio starts playing. A zero duration groups the next asset with the current event.

Example: ASSETS = NoonSkin,Bell,NextSkin and DURATIONS = 0,1,2

At time zero, NoonSkin is shown and Bell starts because their separating duration is zero. After one second, NextSkin is shown. In a loop, it waits two more seconds and returns to the first event.

How live text reaches a sprite

Text assets use two supporting unsandboxed extensions: Temporary Variables stores the live value and style; Animated Text draws it on the destination sprite or clone.

Live value and style are read again every time the asset is shown

Register text Narration, then set its value and optional style. When it is shown, Asset Manager reads the latest temporary variables, reapplies the complete style, and asks Animated Text to display it on the target sprite. Updating a displayed text value refreshes every target that currently shows that asset.

Practical recipes

Common tasks

Show a web image

register resource [https://example.com/card.png]
  as asset [Card]
show asset [Card] on this sprite

The downloaded image is also stored in the browser cache under Card.

Reuse a project costume

register resource [costume:Hero:running]
  as asset [HeroRunning]
show asset [HeroRunning] on this sprite

No image is copied. Asset Manager points to the costume already owned by the project.

Play a sound

register resource [sound:@stage:opening]
  as asset [Opening]
play asset [Opening] as sound until done

Use the non-waiting play block when the next script step should begin immediately.

Display styled live text

register resource [text:Narration]
  as asset [Narration]
set text asset [Narration] to [Once upon a time…]
set text asset [Narration] style [animation] to [typing]
show asset [Narration] on this sprite

Load Temporary Variables and Animated Text before using text display blocks.

Block guide

Choose a block by intent

I want to…Use this blockImportant behavior
Register any sourceregister resource … as asset …A URL refreshes the cache; an empty ID reads it.
Inspect registration failureasset registration error type/labelReturns a stable diagnostic code and relevant name.
Check registrationasset … is loadedTrue for every registered asset type.
Show on a sprite or cloneshow asset … on this spriteAccepts image and text assets.
Change the stageset stage backdrop to asset …Accepts external images, costumes, and backdrops.
Play audioplay asset … as soundUse “until done” when the script must wait.
Animate an actorloop actor … / play actor … onceLists can mix image and audio assets.
Update live textset text asset …Currently displayed targets refresh.
Release registrationsdelete asset … from memoryProject-owned costumes and sounds are not deleted.

Troubleshooting

When something does not appear or play

The external URL fails to load
Confirm the URL uses HTTP or HTTPS, opens directly, and sends a CORS header that permits the TurboWarp editor's origin. The asset registration error type reporter returns REPLACEMENT_FAILED for a failed web request.
A costume, backdrop, or sound cannot be found
Match names exactly. Use the full form such as costume:Hero:running when a shorthand could be ambiguous. Check asset registration error type and asset registration error label to identify the missing project resource. The code is SOURCE_ASSET_NOT_FOUND, or SPRITE_NOT_FOUND when the source sprite itself is missing.
A same-name registration is rejected
ASSET_TYPE_CHANGE means strict replacement found a different public kind, or an external image/audio change. Delete the existing in-memory registration first only when that change is intentional. REPLACEMENT_FAILED means the old registration and managed display were retained because the new content could not be prepared or reapplied.
Text is blank or reports a missing dependency
Load both Temporary Variables and Animated Text without sandboxing. Then set the text value before showing it. A registered text asset with no stored value intentionally displays an empty string.
An animation duration list is rejected
A loop needs one duration per asset. A one-shot sequence needs one fewer duration than assets. Values must be non-negative, and a loop must contain at least one positive duration.
A cached asset cannot be restored
Cache entries are looked up by the exact registered name and are stored in the current browser's IndexedDB. Browser storage cleanup, private sessions, or a different app profile can make an earlier entry unavailable.