UNPKG

@open-game-system/app-bridge-web

Version:

Web-specific implementation of the app-bridge ecosystem

1 lines 12.4 kB
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { applyPatch } from \"fast-json-patch\";\nimport type {\n Bridge,\n BridgeStores,\n Event,\n Store,\n Operation,\n} from \"@open-game-system/app-bridge-types\";\n\nexport type { BridgeStores, State } from \"@open-game-system/app-bridge-types\";\n\n/**\n * Message types for communication\n */\nexport type WebToNativeMessage =\n | { type: \"EVENT\"; storeKey: string; event: Event }\n | { type: \"BRIDGE_READY\" };\n\nexport type NativeToWebMessage<TStores extends BridgeStores = BridgeStores> = {\n type: \"STATE_INIT\";\n storeKey: keyof TStores;\n data: TStores[keyof TStores][\"state\"];\n} | {\n type: \"STATE_UPDATE\";\n storeKey: keyof TStores;\n data?: TStores[keyof TStores][\"state\"];\n operations?: Operation[];\n};\n\nexport interface WebViewBridge {\n postMessage: (message: string) => void;\n}\n\ndeclare global {\n interface Window {\n ReactNativeWebView?: WebViewBridge;\n }\n}\n\n/**\n * Creates a web bridge instance for use in web applications\n * This implementation receives state from the native side through WebView messages\n *\n * @template TStores Store definitions for the bridge\n * @returns A Bridge instance\n */\nexport function createWebBridge<\n TStores extends BridgeStores\n>(): Bridge<TStores> {\n // Internal state storage\n const stateByStore = new Map<\n keyof TStores,\n TStores[keyof TStores][\"state\"]\n >();\n\n // Store instances by key\n const stores = new Map<\n keyof TStores,\n Store<TStores[keyof TStores][\"state\"], TStores[keyof TStores][\"events\"]>\n >();\n\n // Listeners for state changes by store key\n const stateListeners = new Map<\n keyof TStores,\n Set<(state: TStores[keyof TStores][\"state\"]) => void>\n >();\n\n // Listeners for store availability changes\n const storeListeners = new Set<() => void>();\n\n /**\n * Notify all listeners for a specific store's state changes\n */\n const notifyStateListeners = <K extends keyof TStores>(storeKey: K) => {\n const listeners = stateListeners.get(storeKey);\n if (listeners && stateByStore.has(storeKey)) {\n const state = stateByStore.get(storeKey);\n listeners.forEach((listener) => listener(state!));\n }\n };\n\n /**\n * Notify all listeners that a store's availability has changed\n */\n const notifyStoreListeners = () => {\n storeListeners.forEach((listener) => listener());\n };\n\n // Handle messages from native\n if (typeof window !== \"undefined\" && window.ReactNativeWebView) {\n console.log(\"[Web Bridge] ReactNativeWebView detected. Adding message listener and sending BRIDGE_READY.\");\n // Send bridge ready message\n window.ReactNativeWebView.postMessage(JSON.stringify({ type: \"BRIDGE_READY\" }));\n\n const messageHandler = (event: MessageEvent) => {\n // console.log(\"[Web Bridge] Received raw message event:\", event); // Log raw event\n try {\n const message = JSON.parse(event.data) as NativeToWebMessage;\n // console.log(\"[Web Bridge] Parsed message data:\", message); // Log parsed message\n if (message.type === \"STATE_INIT\") {\n // console.log(`[Web Bridge] Handling STATE_INIT for store '${String(message.storeKey)}'`, message.data); // Log init handling\n if (message.data === null) {\n // Remove state when receiving null data\n stateByStore.delete(message.storeKey as keyof TStores);\n } else {\n // Initialize state with full data\n stateByStore.set(message.storeKey as keyof TStores, message.data);\n }\n notifyStateListeners(message.storeKey as keyof TStores);\n notifyStoreListeners();\n } else if (message.type === \"STATE_UPDATE\") {\n // console.log(`[Web Bridge] Handling STATE_UPDATE for store '${String(message.storeKey)}'`, message.operations); // Log update handling\n if (message.data === null) {\n // Remove state when receiving null data\n stateByStore.delete(message.storeKey as keyof TStores);\n notifyStateListeners(message.storeKey as keyof TStores);\n notifyStoreListeners();\n } else if (message.operations) {\n // Apply patch operations\n const currentState = stateByStore.get(\n message.storeKey as keyof TStores\n );\n if (currentState) {\n const result = applyPatch(currentState, message.operations);\n stateByStore.set(\n message.storeKey as keyof TStores,\n result.newDocument\n );\n // console.log(`[Web Bridge] State updated for store '${String(message.storeKey)}' via patch:`, result.newDocument); // Log state after patch\n notifyStateListeners(message.storeKey as keyof TStores);\n }\n }\n }\n } catch (error) {\n console.error(\"[Web Bridge] Error handling message:\", error); // Keep basic error log\n }\n };\n\n window.addEventListener(\"message\", messageHandler);\n\n // Optional: Add cleanup function if the bridge instance can be destroyed\n // () => window.removeEventListener(\"message\", messageHandler);\n\n } else {\n console.warn(\"[Web Bridge] ReactNativeWebView NOT detected.\");\n }\n\n return {\n /**\n * Check if the bridge is supported\n * For web bridge, this checks if ReactNativeWebView is available\n */\n isSupported: () =>\n typeof window !== \"undefined\" && !!window.ReactNativeWebView,\n\n /**\n * Get a store by its key\n * Returns undefined if the store doesn't exist\n */\n getStore: <K extends keyof TStores>(\n storeKey: K\n ): Store<TStores[K][\"state\"], TStores[K][\"events\"]> | undefined => {\n // Only return a store if we have state for it\n if (!stateByStore.has(storeKey)) return undefined;\n\n // Return existing store instance if we have one\n let store = stores.get(storeKey) as\n | Store<TStores[K][\"state\"], TStores[K][\"events\"]>\n | undefined;\n\n // Create a new store if needed\n if (!store) {\n const storeImpl: Store<TStores[K][\"state\"], TStores[K][\"events\"]> = {\n getSnapshot: () => stateByStore.get(storeKey)!,\n subscribe: (listener: (state: TStores[K][\"state\"]) => void) => {\n if (!stateListeners.has(storeKey)) {\n stateListeners.set(storeKey, new Set());\n }\n const listeners = stateListeners.get(storeKey)!;\n listeners.add(listener);\n\n // Notify immediately with current state\n if (stateByStore.has(storeKey)) {\n listener(stateByStore.get(storeKey)!);\n }\n\n return () => {\n listeners.delete(listener);\n };\n },\n dispatch: async (event: TStores[K][\"events\"]): Promise<void> => {\n console.log(`[Web Bridge] Dispatching event for store ${String(storeKey)}:`, event);\n if (!window.ReactNativeWebView) {\n console.warn(\n \"[Web Bridge] Cannot dispatch events: ReactNativeWebView not available\"\n );\n return;\n }\n const message: WebToNativeMessage = {\n type: \"EVENT\",\n storeKey: storeKey as string,\n event,\n };\n console.log(\"[Web Bridge] Sending message to native:\", message);\n window.ReactNativeWebView.postMessage(JSON.stringify(message));\n },\n reset: () => {\n // For web bridge, reset is a no-op since state is managed by native\n console.warn(\"[Web Bridge] Reset operation not supported in web bridge\");\n },\n // Add 'on' method to satisfy the interface\n on: <EventType extends TStores[K][\"events\"]['type']>(\n eventType: EventType,\n _listener: (\n event: Extract<TStores[K][\"events\"], { type: EventType }>,\n store: Store<TStores[K][\"state\"], TStores[K][\"events\"]>\n ) => Promise<void> | void\n ): (() => void) => {\n console.warn(`[Web Bridge] store.on(\"${eventType}\", ...) was called, but listeners added on the web side are not executed. Add listeners on the native side or via the store's 'on' config.`);\n // Return a no-op unsubscribe function\n return () => {};\n }\n };\n stores.set(storeKey, storeImpl);\n store = storeImpl;\n }\n\n return store;\n },\n\n /**\n * Set or remove a store for a given key\n */\n setStore: <K extends keyof TStores>(\n key: K,\n store: Store<TStores[K][\"state\"], TStores[K][\"events\"]> | undefined\n ) => {\n if (store === undefined) {\n stores.delete(key);\n stateByStore.delete(key);\n } else {\n stores.set(key, store);\n const snapshot = store.getSnapshot();\n if (snapshot !== undefined) {\n stateByStore.set(key, snapshot);\n }\n }\n notifyStoreListeners();\n },\n\n /**\n * Subscribe to store availability changes\n * Returns an unsubscribe function\n */\n subscribe: (listener: () => void) => {\n storeListeners.add(listener);\n return () => {\n storeListeners.delete(listener);\n };\n },\n };\n} "],"mappings":"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","names":[]}