|
| 1 | +export type NsHmrUpdatePayload = { |
| 2 | + type: 'full-graph' | 'delta'; |
| 3 | + version: number; |
| 4 | + changedIds: string[]; |
| 5 | + // Raw message payload from the HMR WebSocket |
| 6 | + raw: any; |
| 7 | +}; |
| 8 | + |
| 9 | +export type NsHmrUpdateHandler = (payload: NsHmrUpdatePayload) => void; |
| 10 | + |
| 11 | +type NsHmrGlobalState = { |
| 12 | + __NS_HMR_ON_UPDATE__?: unknown; |
| 13 | + __NS_HMR_ON_UPDATE_DISPATCHER__?: NsHmrUpdateHandler; |
| 14 | + __NS_HMR_ON_UPDATE_REGISTRY__?: Map<string, NsHmrUpdateHandler>; |
| 15 | + __NS_HMR_ON_UPDATE_BASE__?: unknown; |
| 16 | +}; |
| 17 | + |
| 18 | +function getNsHmrGlobal(): NsHmrGlobalState { |
| 19 | + return globalThis as any; |
| 20 | +} |
| 21 | + |
| 22 | +function ensureDispatcherInstalled(): { |
| 23 | + registry: Map<string, NsHmrUpdateHandler>; |
| 24 | + dispatcher: NsHmrUpdateHandler; |
| 25 | + base: unknown; |
| 26 | +} { |
| 27 | + const g = getNsHmrGlobal(); |
| 28 | + if (!g.__NS_HMR_ON_UPDATE_REGISTRY__) g.__NS_HMR_ON_UPDATE_REGISTRY__ = new Map(); |
| 29 | + const registry = g.__NS_HMR_ON_UPDATE_REGISTRY__; |
| 30 | + |
| 31 | + if (!g.__NS_HMR_ON_UPDATE_DISPATCHER__) { |
| 32 | + const base = g.__NS_HMR_ON_UPDATE__; |
| 33 | + // If something already owns the hook and it's not our dispatcher, preserve it. |
| 34 | + g.__NS_HMR_ON_UPDATE_BASE__ = base; |
| 35 | + g.__NS_HMR_ON_UPDATE_DISPATCHER__ = (payload: NsHmrUpdatePayload) => { |
| 36 | + // Call registered handlers first (app-level consumers). |
| 37 | + try { |
| 38 | + for (const handler of registry.values()) { |
| 39 | + try { |
| 40 | + handler(payload); |
| 41 | + } catch {} |
| 42 | + } |
| 43 | + } catch {} |
| 44 | + // Then call any preserved base hook. |
| 45 | + try { |
| 46 | + const b = (getNsHmrGlobal() as any).__NS_HMR_ON_UPDATE_BASE__; |
| 47 | + if (typeof b === 'function') (b as NsHmrUpdateHandler)(payload); |
| 48 | + } catch {} |
| 49 | + }; |
| 50 | + g.__NS_HMR_ON_UPDATE__ = g.__NS_HMR_ON_UPDATE_DISPATCHER__; |
| 51 | + } |
| 52 | + |
| 53 | + return { |
| 54 | + registry, |
| 55 | + dispatcher: g.__NS_HMR_ON_UPDATE_DISPATCHER__!, |
| 56 | + base: g.__NS_HMR_ON_UPDATE_BASE__, |
| 57 | + }; |
| 58 | +} |
| 59 | + |
| 60 | +/** |
| 61 | + * Register a callback that will be invoked after each HMR batch |
| 62 | + * (full graph or delta) is applied on device. |
| 63 | + * |
| 64 | + * It is safe to call multiple times with the same `id`; the handler |
| 65 | + * will be replaced instead of stacking duplicates across module reloads. |
| 66 | + */ |
| 67 | + |
| 68 | +export function onHmrUpdate(handler: NsHmrUpdateHandler, id: string): void { |
| 69 | + if (typeof handler !== 'function') return; |
| 70 | + if (typeof id !== 'string' || !id) return; |
| 71 | + try { |
| 72 | + const { registry } = ensureDispatcherInstalled(); |
| 73 | + registry.set(id, handler); |
| 74 | + } catch {} |
| 75 | +} |
| 76 | + |
| 77 | +/** Remove a previously registered handler (use the same `id` you registered with). */ |
| 78 | +export function offHmrUpdate(id: string): void { |
| 79 | + if (typeof id !== 'string' || !id) return; |
| 80 | + try { |
| 81 | + const g = getNsHmrGlobal(); |
| 82 | + g.__NS_HMR_ON_UPDATE_REGISTRY__?.delete(id); |
| 83 | + } catch {} |
| 84 | +} |
0 commit comments