@balancy/bridge
v1.9.5
Published
Balancy WebView Bridge - Unity-like component system for HTML/JavaScript
Downloads
600
Readme
Balancy WebView Bridge - TypeScript Package
This package contains the TypeScript implementation of the Balancy WebView Bridge, which has been migrated from a single JavaScript file to a modular TypeScript codebase.
Phase 1 Complete ✅
The initial setup and migration have been completed. Both the bridge file and styles file have been merged into a single TypeScript package:
- Original files: 40KB (bridge) + 24KB (styles) = 64KB total
- New build: 31.91KB minified (50% size reduction)
- Single injection: Instead of injecting 2 files, now you only inject 1
- Auto-copy: Build automatically copies to core package for SDK inclusion
Project Structure
packages/bridge/
├── dist/ # Build output
│ └── balancy-webview-bridge.js # Minified bundle
├── src/
│ ├── index.ts # Main entry point
│ ├── types.ts # Type definitions
│ ├── constants.ts # Enums and constants
│ ├── messaging/ # Request/response handling
│ │ ├── BatchManager.ts
│ │ ├── messageHandler.ts
│ │ └── requestHandler.ts
│ ├── cache/ # Caching systems
│ │ ├── ImageCache.ts
│ │ └── LocalizationCache.ts
│ ├── api/ # API method groups
│ │ ├── profile.ts
│ │ ├── offers.ts
│ │ ├── tasks.ts
│ │ ├── inventory.ts
│ │ ├── battlepass.ts
│ │ └── events.ts
│ ├── ui/ # UI preparation methods
│ │ ├── localization.ts
│ │ ├── images.ts
│ │ ├── fonts.ts
│ │ ├── buttons.ts
│ │ └── dynamicText.ts
│ ├── styles/ # CSS & performance optimizations
│ │ └── gameUI.ts # Game UI styles & behaviors
│ ├── utils/ # Utility functions
│ │ ├── formatting.ts
│ │ ├── templates.ts
│ │ └── dom.ts
│ ├── components/ # Component system (Phase 2+)
│ ├── serialization/ # Serialization system (Phase 3+)
│ ├── managers/ # Managers (Phase 4+)
│ └── initialization.ts # Initialization flow
├── scripts/
│ └── build.js # esbuild configuration
├── package.json
├── tsconfig.json
└── README.mdBuild Scripts
Install Dependencies
cd packages/bridge
npm installProduction Build
npm run buildOutputs minified bundle to dist/balancy-webview-bridge.js
Development Build
npm run build:devOutputs bundle with inline source maps for debugging
Watch Mode
npm run watchAutomatically rebuilds on file changes
Key Improvements
- Modular Architecture: Code is organized into logical modules by domain
- Type Safety: Full TypeScript typing with strict mode enabled
- Better Minification: 50% size reduction (64KB → 32KB)
- JavaScript minification via esbuild
- CSS minification in post-processing
- Single File Bundle: Combined bridge + styles into one file (no more dual injection)
- Auto-copy to SDK: Build automatically updates the core package file
- Maintainability: Easier to navigate and modify specific functionality
- Build Pipeline: Fast, modern build system with esbuild
Current Status
✅ Phase 1 Complete: TypeScript setup and migration
- All existing functionality preserved (bridge + styles)
- Build pipeline operational with CSS minification
- Code organized into modules
- Single unified bundle (31.91KB - 50% reduction)
- Auto-copy to core package on build
- Ready for Phase 2 implementation
Next Steps
Phase 2 will implement the Unity-like component system:
- ElementBehaviour base class
- ElementObject wrapper
- Script registry
- GUID management
- And more...
Testing
Before proceeding to Phase 2, test the current build:
- The bundle is automatically built to:
- Build output:
packages/bridge/dist/balancy-webview-bridge.js - Auto-copied to:
packages/core/src/webview/resources/balancy-webview-bridge.js
- Build output:
- Original files (now replaced):
- ~~
packages/core/src/webview/resources/balancy-webview-bridge.js~~ (40KB) - ~~
packages/core/src/webview/resources/balancy-webview-styles.js~~ (24KB)
- ~~
- The new bundle (31.91KB) automatically replaces the old bridge file on build
- You can now remove the styles file injection - everything is in one file
- Compare functionality to ensure identical behavior
Technical Notes
- Target: ES2020
- Module Format: IIFE (for browser global access)
- Global:
window.balancy - Minifier: esbuild (JavaScript) + custom CSS minifier
- TypeScript: Strict mode enabled
- Build Features:
- Automatic CSS minification in post-processing
- Auto-copy to core package on production builds
- Auto-copy to Unity plugin (
plugin_cpp_unity) on production builds - Watch mode skips auto-copy (for development)
- Development builds skip CSS minification
Build Copy Targets
The build script (scripts/build.js) copies the output to two locations:
- Core package:
packages/core/src/webview/resources/balancy-webview-bridge.js - Unity plugin:
plugin_cpp_unity/WebView/Resources/balancy-webview-bridge.txt
The Unity path is relative — it expects plugin_cpp_unity to be cloned as a sibling of plugin_cpp_typescript:
Projects/
plugin_cpp_typescript/ <-- this repo
plugin_cpp_unity/ <-- Unity plugin repo (sibling)The Unity copy silently skips if the directory doesn't exist (wrapped in try/catch). Copying is also skipped in watch mode.
Persistent View ownership and cleanup
A persistent shell keeps reusable resources across opens: raw prefab templates, bounded resource caches, script classes and browser-managed image/font data. A template is not a live instance. Browser decoded texture memory is managed by the browser and is not guaranteed to remain resident.
On View close, the SDK destroys managed components and instances, calls their onDestroy, cancels view requests, clears the mount, and stops the component update loop. Instances created through ElementsManager.instantiate / instantiatePrefab belong to that View even if detached or moved outside its parent; the SDK removes them on close. Do not instantiate new objects from cleanup callbacks.
Arbitrary DOM created by document.createElement outside the View mount is not automatically owned by the SDK. Register a disposer. Also explicitly release custom observers, animation-frame loops, workers, sockets, global references, and other external resources. A detached object without reachable references can be garbage-collected; an object attached to document.body, stored globally, or retained by a callback remains reachable.
const overlay = document.createElement('div');
document.body.append(overlay);
const observer = new ResizeObserver(() => { /* update overlay */ });
observer.observe(overlay);
balancy.onViewDispose(() => {
observer.disconnect();
overlay.remove();
});Capture the current window.balancyViewSignal before custom asynchronous work and check signal.aborted after each await before creating UI. Registering cleanup only after a View has closed is too late. Never keep a View's DOM/component in a static/global collection unless you remove it on disposal.
Checking memory
- After clear,
balancy.ElementsManager.getLifecycleStats()must reportelements: 0,instances: 0,pendingPreparations: 0,updateLoopRunning: false. - With performance logging enabled,
viewDisposedincludesliveElementsAfterClear,liveInstancesAfterClear,pendingPreparationsAfterClear,pendingRequestsAfterClear, andprefabTemplatesCached. The first four should return to zero; retained templates are expected. node packages/bridge/scripts/check-memory.cjs --performancefrom the repository root runs actual Chromium, 520 persistent cycles (two prefab instances each), standalone TypeScript and Unity WebGL adapter cycles, and explicit GC reachability checks.CHROME_BINoverrides the default macOS Chrome path.- Reopen a fixed set of Views repeatedly after warmup. Compare post-GC retained objects, DOM/listeners and heap plateau, not just process RSS. Distinct resources can legitimately grow caches until their budgets are reached.
- Counters verify SDK bookkeeping. Heap reachability catches forgotten references. Physical-device native/GPU/process memory still needs a separate profiler run with customer content.
Instance timers
balancy.createTimer(instanceId)
Creates an independent countdown for an active event, offer, or offer group. Battle Pass events use the same API. Pass the runtime instanceId from EventInfo, OfferInfo, or OfferGroupInfo, not a configuration unnyId. The SDK resolves the type automatically; this API never falls back to the View owner.
Returns: Promise<ViewTimer>.
timer.instanceId: the instance this timer belongs to.timer.getTimeLeft(): synchronous remaining whole seconds, rounded up and clamped to0;nullmeans no time limit.await timer.refresh(): fetch a new snapshot, for example after an event extension or returning from a long background pause. Concurrent refreshes share one request. A failed refresh rejects and keeps the previous snapshot.
Creation makes one native request. Reads count down locally from the received snapshot using a monotonic clock; they do not contact the SDK. Transport time is not subtracted from the snapshot. The timer does not automatically track event changes or removal. Refresh rejects if the instance no longer exists; invalid/empty IDs and unsupported native SDK versions also reject.
No intervals or subscriptions are created, and no dispose() is needed. In persistent mode, refreshing a timer from a disposed View rejects. Create a new timer for the next View. Use a matching native SDK and bridge build that support this API.
class OfferTimer extends balancy.ElementBehaviour {
// @serialize {element}
timerText = null;
timer = null;
async init(offerInfo) {
this.timer = await balancy.createTimer(offerInfo.instanceId);
}
update() {
if (!this.timer) return;
const seconds = this.timer.getTimeLeft();
this.timerText.element.textContent = seconds === null
? '' : balancy.formatTime(seconds);
}
}For an event or group, pass eventInfo.instanceId or offerGroupInfo.instanceId instead. Each instantiated card owns its own timer, so a shop can display several independent countdowns.
View instance context
New View scripts must pass a runtime instanceId, not a configuration unnyId / unnyIdGameEvent. Obtain it from the EventInfo, OfferInfo, or OfferGroupInfo passed to your root script's init(info). When instantiating a child card, pass that card's own info to its init(info); do not borrow the parent View owner.
Explicit IDs are strict: unknown IDs, configuration IDs and invalid values do not fall back to the global owner. Event operations require an event instance; an offer instance is not an event instance. Custom event info supports event, offer and group instances and keeps their data separate.
| Method | New signature |
| --- | --- |
| Tasks | getTasks(instanceId) |
| Activate/deactivate tasks | activateTasks(taskIds, instanceId), deactivateTasks(taskIds, instanceId) |
| Battle Pass | getBattlePassConfig(instanceId), getBattlePassProgress(instanceId) |
| Battle Pass reward | claimBattlePassReward(lineId, index, instanceId) |
| Custom event info | getCustomEventInfo(instanceId), setCustomEventInfo(data, instanceId) |
| Stop event | stopEventManually(cooldown, instanceId); pass -1 for the configured cooldown |
| Offer purchase | buyOffer(instanceId) |
| Group purchase | buyGroupOffer(instanceId, index), canBuyGroupOffer(index, instanceId) |
| Timer | await createTimer(instanceId) |
Only the context argument changes. Task IDs in taskIds / claimTaskReward(taskId), reward line IDs, product IDs and getCustomDocumentInfo(unnyId) / setCustomDocumentInfo(unnyId, data) retain their existing meanings. These bridge APIs are JavaScript APIs inside the View on both Unity and web; they do not change the host C# API.
Examples below use eventInfo, offerInfo and offerGroupInfo for the corresponding runtime info objects supplied to init. A matching updated native SDK and bridge are required.
