@omni-gate/frame-client-sdk
v0.3.0
Published
Frame-side (iframe/child window) SDK for Omni Gate sub-applications: heartbeat, JWT relay, module navigation and message forwarding to the parent shell. This is the package the many sub-apps install.
Downloads
214
Maintainers
Readme
@omni-gate/frame-client-sdk
The frame-side (iframe / child window) half of the Omni Gate frame-bridge SDK. This is the package a sub-application running inside the platform shell installs — one shell, many sub-apps, and this is what the sub-apps use.
It only talks to the parent window through a message bridge; it never calls
a REST API directly and carries no parent-only dependency (no dexie, no
JWT-expiry monitor). For the parent/shell counterpart, see
@omni-gate/frame-parent-sdk.
简体中文 | English
Installation
npm install @omni-gate/frame-client-sdk @ticatec/iframe-message-bridgeThis package re-exports everything from
@omni-gate/frame-core (Permissions, BaseRestServiceProxy,
i18nRes, constants), so most sub-apps only need to install this one
package.
Quick Start
import OmniClientApi from '@omni-gate/frame-client-sdk';
import { MessageBridgeClient } from '@ticatec/iframe-message-bridge';
// Create the message bridge client, pointing at the parent window's origin
const bridge = new MessageBridgeClient('https://shell.example.com');
// Initialize the client API (singleton)
const api = await OmniClientApi.initialize(bridge);
const me = await api.getMe();OmniClientApi starts a heartbeat as soon as it's initialized (default
interval: 10s) and listens for user interaction (keydown, keyup,
mousemove, mousedown, mouseup, click) to know whether to keep sending
it, so the parent can track whether the sub-app tab is actually alive.
API Reference
OmniClientApi.initialize(bridge, options?)
Creates (once) and returns the singleton instance, wrapped in a Promise.
Same-origin (the platform's default, alias-based deployment): the Promise
resolves immediately. Parent and sub-app share the same sessionStorage/
localStorage, so BaseRestServiceProxy in the sub-app already sees
whatever the parent writes there — OmniClientApi doesn't need to do
anything.
Cross-origin: parent and sub-app storage are separate, so initialize()
makes one round trip to the parent for the current JWT and writes it into
the sub-app's own storage before the Promise resolves — await it before
issuing your first REST call. From then on, OmniClientApi listens for the
parent's token-renewal and logoff broadcasts and keeps that local copy in
sync automatically; you don't need to call getJwtToken() yourself just to
feed BaseRestServiceProxy.
const options = {
jwtStorage: sessionStorage, // where to write the token in cross-origin mode (default: sessionStorage)
jwtStorageKey: 'omni-token', // storage key (default: 'omni-token')
};
const api = await OmniClientApi.initialize(bridge, options);options only matters for cross-origin deployments, and only to pick where
the synced token is written. It must match whatever jwtStorage/
jwtStorageKey your sub-app's own BaseRestServiceProxy subclass uses —
otherwise the two will read and write different storage/keys and the token
will never be found.
OmniClientApi.getInstance()
Returns the singleton instance created by initialize(). Throws if called
before initialize() or after destroy() — it never returns null, so you
don't need to null-check every call site.
sameOrigin / crossOrigin
Read-only getters. The SDK detects at construction time whether it can read
window.parent.location.origin (same-origin) or not (cross-origin).
The platform's committed deployment model is same-origin, alias-based
hosting, and most of the JWT/logoff behavior described below is written
with that in mind. OmniClientApi also handles the cross-origin case (see
initialize() above): it keeps a local copy of the JWT in sync via
broadcasts instead of relying on shared storage. What cross-origin sub-apps
still don't get for free is everything else this SDK proxies through the
parent (getMe, getPermission, getOptionsData, ...) — those already go
over the bridge either way, so they're unaffected either way.
User / permission / dictionary reads (all proxied to the parent)
getMe<T = any>(): Promise<T>— current user, viaOmniParentApi.getMe()on the other side.getPermission<T = Record<string, any>>(appCode: string): Promise<T>— application-level permission map.getEntityPermission<T = Record<string, any>>(appCode: string, entityCode: string): Promise<T>— entity-level permission map.getOptionsData<T = any>(dicNames: string | string[]): Promise<T>— one or more data-dictionary option lists.getChildrenOptions<T = any>(dic: string, code: string): Promise<T>— child options of a hierarchical dictionary node.getErrorMessage(serviceCode: string, errorCode: string): Promise<string>— localized text for a single error code (language is decided by the parent). Only the resolved string crosses the bridge — the parent keeps its own full error-message table to itself. Returns a fallback "error code not found" message instead ofundefinedwhen the code isn't in the table.getCurrentLanguage(): Promise<string>— current language code (e.g.'en','zh-CN'), falling back to'en'.
Each generic type parameter defaults to the untyped shape shown above, so
existing call sites keep working unchanged; a sub-app that knows its own
data shape can specify it explicitly, e.g. api.getMe<CurrentUser>().
All of these simply call bridge.emit(...) and return {} (or []) if the
parent doesn't answer — they never throw for a missing parent response
(getCurrentLanguage() is the one exception, returning the plain string
'en' as its fallback rather than an empty object).
openModule(modHash: string, params: any): void
Fire-and-forget message asking the parent shell to open another module.
JWT token handling
await api.getJwtToken(); // always asks the parent window; no local read/cache of its owngetJwtToken() never reads or caches locally — every call goes straight to
the parent. It's meant for the rare case where you need the raw token string
yourself (e.g. to hand to a non-HTTP client); BaseRestServiceProxy doesn't
use it.
BaseRestServiceProxy's request interceptor is synchronous, so it can't
await a bridge round trip on every outgoing request — it reads the token
straight out of storage.getItem(...). Same-origin, that's free: sub-apps
are deployed as path aliases under the shell's own domain (not subdomains),
so the sub-app's sessionStorage/localStorage is the shell's storage,
and whatever the parent writes (initial login, token renewal) is visible
immediately, no sync needed. Cross-origin, that storage is the sub-app's
own and starts out empty; initialize() (see above) seeds it from the
parent once and keeps it current via broadcasts, so BaseRestServiceProxy
still finds a fresh token there without knowing or caring which deployment
mode it's running in.
Logoff handling
Sub-apps hold no local copy of anything the parent owns — not the JWT token,
not getMe/getPermission/getOptionsData results. Same-origin, the
parent (OmniParentApi.broadcastLogoff()) clears the JWT token from that
same shared storage before broadcasting, so by the time the sub-app's
LOGOFF handler runs, there's nothing local left for it to clean up.
Cross-origin, the parent's cleanup only touches its own storage, so
OmniClientApi clears its own synced copy when the LOGOFF broadcast
arrives — this happens automatically, onLogoff() below is purely for your
own UI reaction.
api.onLogoff((data) => {
console.log('logged off at', data.timestamp);
router.push('/login');
});onLogoff() is purely a notification hook now (for UI reactions like
redirecting to a login screen) — it no longer clears storage by default.
Broadcasts
api.onBroadcast('theme-changed', (data) => applyTheme(data.theme));
api.offBroadcast('theme-changed');
api.clearBroadcastHandlers();destroy(): void
Stops the heartbeat timer, removes DOM/message listeners, and clears the singleton instance.
TypeScript
Fully typed; the shared types come from @omni-gate/frame-core and are
re-exported here.
Dependencies
@omni-gate/frame-core(workspace dependency)@ticatec/iframe-message-bridge— peer dependency.OmniClientApi.initialize(bridge)takes aMessageBridgeClientinstance you construct yourself, so this package doesn't bundle its own copy — install it alongside@omni-gate/frame-client-sdk:npm install @omni-gate/frame-client-sdk @ticatec/iframe-message-bridge
License
MIT
